diff --git a/Cargo.lock b/Cargo.lock index e811136a24..7623e045db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6217,6 +6217,7 @@ dependencies = [ "pallet-balances", "pallet-bonds", "pallet-broadcast", + "pallet-btc-vault", "pallet-circuit-breaker", "pallet-claims", "pallet-collator-rewards", @@ -9560,6 +9561,27 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-btc-vault" +version = "0.1.0" +dependencies = [ + "alloy-sol-types 0.7.7", + "borsh", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "pallet-balances", + "pallet-signet", + "parity-scale-codec", + "scale-info", + "serde_json", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-child-bounties" version = "39.0.0" @@ -11568,6 +11590,7 @@ dependencies = [ "parity-scale-codec", "rlp 0.6.1", "scale-info", + "signet-rs", "sp-core", "sp-io", "sp-runtime", @@ -17416,6 +17439,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-hex-utils" version = "0.1.0" @@ -17599,6 +17631,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signet-rs" +version = "0.2.5" +source = "git+https://github.com/esaminu/signet.rs?branch=borsh-1.5.7#d85f0e81507c315ae65e08b9c28f172baf9a59ce" +dependencies = [ + "borsh", + "bs58", + "hex", + "rlp 0.6.1", + "serde", + "serde-big-array", + "serde_json", + "sha2 0.10.8", +] + [[package]] name = "simba" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index e437bd2403..c686b84b95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,8 @@ members = [ 'liquidation-worker-support', 'pallets/hsm', "pallets/signet", - "pallets/dispenser" + "pallets/dispenser", + "pallets/btc-vault" ] resolver = "2" @@ -163,6 +164,7 @@ pallet-hsm = { path = "pallets/hsm", default-features = false } pallet-parameters = { path = "pallets/parameters", default-features = false } pallet-signet = { path = "pallets/signet", default-features = false } pallet-dispenser = { path = "pallets/dispenser", default-features = false } +pallet-btc-vault = { path = "pallets/btc-vault", default-features = false } hydra-dx-build-script-utils = { path = "utils/build-script-utils", default-features = false } scraper = { path = "scraper", default-features = false } diff --git a/launch-configs/chopsticks/polkadot.yml b/launch-configs/chopsticks/polkadot.yml index 723e9290eb..94d09b19ce 100644 --- a/launch-configs/chopsticks/polkadot.yml +++ b/launch-configs/chopsticks/polkadot.yml @@ -17,4 +17,4 @@ import-storage: TechnicalCommittee: Members: [5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY] TechnicalMembership: - Members: [5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY] + Members: [5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY] \ No newline at end of file diff --git a/pallets/btc-vault/Cargo.toml b/pallets/btc-vault/Cargo.toml new file mode 100644 index 0000000000..eaa02016dd --- /dev/null +++ b/pallets/btc-vault/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pallet-btc-vault" +version = "0.1.0" +edition = "2021" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true } +scale-info = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } +pallet-signet = { workspace = true } +serde_json = { workspace = true } +hex = { workspace = true } +borsh = { workspace = true } +alloy-sol-types = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true, features = ["std"] } +sp-core = { workspace = true, features = ["std"] } +sp-runtime = { workspace = true, features = ["std"] } +pallet-balances = { workspace = true, features = ["std"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-std/std", + "sp-core/std", + "sp-io/std", + "frame-benchmarking?/std", + "pallet-signet/std", + "serde_json/std", + "hex/std", + "alloy-sol-types/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-signet/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/btc-vault/src/benchmarking.rs b/pallets/btc-vault/src/benchmarking.rs new file mode 100644 index 0000000000..77db902b23 --- /dev/null +++ b/pallets/btc-vault/src/benchmarking.rs @@ -0,0 +1,290 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::v2::*; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use sp_runtime::traits::AccountIdConversion; + +fn bench_chain_id() -> BoundedVec::MaxChainIdLength> { + let v: Vec = b"bench-chain".to_vec(); + BoundedVec::try_from(v).expect("bench-chain fits MaxChainIdLength") +} + +type BalanceOf = <::Currency as frame_support::traits::Currency< + ::AccountId, +>>::Balance; + +#[benchmarks(where T: Config)] +mod benches { + use super::*; + use core::ops::{Add, Mul}; + use frame_support::traits::Currency; + + #[benchmark] + fn pause() { + PalletConfig::::put(PalletConfigData { paused: false }); + + #[extrinsic_call] + pause(RawOrigin::Root); + + assert!(PalletConfig::::get().unwrap().paused); + } + + #[benchmark] + fn unpause() { + PalletConfig::::put(PalletConfigData { paused: true }); + + #[extrinsic_call] + unpause(RawOrigin::Root); + + assert!(!PalletConfig::::get().unwrap().paused); + } + + #[benchmark] + fn request_deposit() { + let signet_admin: T::AccountId = whitelisted_caller(); + let chain_id = super::bench_chain_id::(); + + let pallet_account: T::AccountId = Pallet::::account_id(); + let signet_pallet_account: T::AccountId = + ::PalletId::get().into_account_truncating(); + + let ed_native: BalanceOf = ::Currency::minimum_balance(); + assert_ok!(pallet_signet::Pallet::::initialize( + RawOrigin::Root.into(), + signet_admin, + ed_native, + chain_id, + )); + + let requester_needed: BalanceOf = ed_native.add(ed_native.mul(10u32.into())); + let _ = + ::Currency::deposit_creating(&pallet_account, requester_needed); + let _ = ::Currency::deposit_creating( + &signet_pallet_account, + requester_needed, + ); + + let caller: T::AccountId = whitelisted_caller(); + let _ = ::Currency::deposit_creating(&caller, requester_needed); + + let vault_pubkey_hash = T::VaultPubkeyHash::get(); + let mut vault_script = Vec::with_capacity(22); + vault_script.push(0x00); + vault_script.push(0x14); + vault_script.extend_from_slice(&vault_pubkey_hash); + + let input = pallet_signet::UtxoInput { + txid: [1u8; 32], + vout: 0, + value: 100_000, + script_pubkey: BoundedVec::try_from(vault_script.clone()).unwrap(), + sequence: 0xffffffff, + }; + let inputs: BoundedVec = + BoundedVec::try_from(vec![input]).unwrap(); + + let output = pallet_signet::BitcoinOutput { + value: 90_000, + script_pubkey: BoundedVec::try_from(vault_script).unwrap(), + }; + let outputs: BoundedVec = + BoundedVec::try_from(vec![output]).unwrap(); + + let lock_time = 0u32; + + let txid = pallet_signet::Pallet::::get_txid( + RawOrigin::Signed(caller.clone()).into(), + inputs.clone(), + outputs.clone(), + lock_time, + ) + .expect("get_txid ok in benchmark"); + + let path: Vec = { + let enc = pallet_account.encode(); + let mut s = Vec::with_capacity(2 + enc.len() * 2); + s.extend_from_slice(b"0x"); + s.extend_from_slice(hex::encode(enc).as_bytes()); + s + }; + + let request_id = Pallet::::generate_request_id( + &pallet_account, + txid.as_ref(), + T::BitcoinCaip2::get(), + T::KeyVersion::get(), + &path, + ECDSA, + BITCOIN, + b"", + ); + + #[extrinsic_call] + request_deposit(RawOrigin::Signed(caller), request_id, inputs, outputs, lock_time); + } + + #[benchmark] + fn claim_deposit() { + let caller: T::AccountId = whitelisted_caller(); + + let request_id: Bytes32 = [42u8; 32]; + let amount_sats: u64 = 50_000; + PendingDeposits::::insert( + request_id, + PendingDepositData { + requester: caller.clone(), + amount_sats, + txid: [1u8; 32], + path: BoundedVec::try_from(b"0xdeadbeef".to_vec()).unwrap(), + }, + ); + + let serialized_output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + let dummy_signature = pallet_signet::Signature { + big_r: pallet_signet::AffinePoint { + x: [0u8; 32], + y: [0u8; 32], + }, + s: [0u8; 32], + recovery_id: 0, + }; + + #[extrinsic_call] + claim_deposit(RawOrigin::Signed(caller.clone()), request_id, serialized_output, dummy_signature); + + assert_eq!(UserBalances::::get(&caller), amount_sats); + assert!(PendingDeposits::::get(request_id).is_none()); + } + + #[benchmark] + fn withdraw_btc() { + let signet_admin: T::AccountId = whitelisted_caller(); + let chain_id = super::bench_chain_id::(); + + let pallet_account: T::AccountId = Pallet::::account_id(); + let signet_pallet_account: T::AccountId = + ::PalletId::get().into_account_truncating(); + + let ed_native: BalanceOf = ::Currency::minimum_balance(); + assert_ok!(pallet_signet::Pallet::::initialize( + RawOrigin::Root.into(), + signet_admin, + ed_native, + chain_id, + )); + + let requester_needed: BalanceOf = ed_native.add(ed_native.mul(10u32.into())); + let _ = + ::Currency::deposit_creating(&pallet_account, requester_needed); + let _ = ::Currency::deposit_creating( + &signet_pallet_account, + requester_needed, + ); + + let caller: T::AccountId = whitelisted_caller(); + let _ = ::Currency::deposit_creating(&caller, requester_needed); + + let vault_pubkey_hash = T::VaultPubkeyHash::get(); + let mut vault_script = Vec::with_capacity(22); + vault_script.push(0x00); + vault_script.push(0x14); + vault_script.extend_from_slice(&vault_pubkey_hash); + + let recipient_script = vault_script.clone(); + + let input = pallet_signet::UtxoInput { + txid: [1u8; 32], + vout: 0, + value: 100_000, + script_pubkey: BoundedVec::try_from(vault_script.clone()).unwrap(), + sequence: 0xffffffff, + }; + let inputs: BoundedVec = + BoundedVec::try_from(vec![input]).unwrap(); + + let output = pallet_signet::BitcoinOutput { + value: 90_000, + script_pubkey: BoundedVec::try_from(vault_script).unwrap(), + }; + let outputs: BoundedVec = + BoundedVec::try_from(vec![output]).unwrap(); + + let lock_time = 0u32; + let amount = 90_000u64; + + // Give user some BTC balance to withdraw + UserBalances::::insert(&caller, amount); + + let txid = pallet_signet::Pallet::::get_txid( + RawOrigin::Signed(caller.clone()).into(), + inputs.clone(), + outputs.clone(), + lock_time, + ) + .expect("get_txid ok in benchmark"); + + let request_id = Pallet::::generate_request_id( + &pallet_account, + txid.as_ref(), + T::BitcoinCaip2::get(), + T::KeyVersion::get(), + WITHDRAWAL_PATH, + ECDSA, + BITCOIN, + b"", + ); + + #[extrinsic_call] + withdraw_btc( + RawOrigin::Signed(caller.clone()), + request_id, + amount, + BoundedVec::try_from(recipient_script).unwrap(), + inputs, + outputs, + lock_time, + ); + + assert_eq!(UserBalances::::get(&caller), 0); + assert!(PendingWithdrawals::::get(request_id).is_some()); + } + + #[benchmark] + fn complete_withdraw_btc() { + let caller: T::AccountId = whitelisted_caller(); + + let request_id: Bytes32 = [42u8; 32]; + let amount_sats: u64 = 50_000; + UserBalances::::insert(&caller, 100_000u64); + PendingWithdrawals::::insert( + request_id, + PendingWithdrawalData { + requester: caller.clone(), + amount_sats, + }, + ); + + let serialized_output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + let dummy_signature = pallet_signet::Signature { + big_r: pallet_signet::AffinePoint { + x: [0u8; 32], + y: [0u8; 32], + }, + s: [0u8; 32], + recovery_id: 0, + }; + + #[extrinsic_call] + complete_withdraw_btc(RawOrigin::Signed(caller.clone()), request_id, serialized_output, dummy_signature); + + assert!(PendingWithdrawals::::get(request_id).is_none()); + } + + impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/pallets/btc-vault/src/lib.rs b/pallets/btc-vault/src/lib.rs new file mode 100644 index 0000000000..b83ebd2d78 --- /dev/null +++ b/pallets/btc-vault/src/lib.rs @@ -0,0 +1,607 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; +use alloc::vec; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::*; +use frame_support::PalletId; +use frame_system::pallet_prelude::*; +use sp_runtime::traits::{AccountIdConversion, Saturating}; +use sp_std::vec::Vec; + +pub mod benchmarking; +pub mod types; +pub mod weights; + +#[cfg(test)] +pub mod tests; + +pub use pallet::*; +pub use types::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::dispatch::DispatchResult; + use frame_support::traits::Currency; + use sp_io::hashing; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_signet::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type UpdateOrigin: EnsureOrigin; + + #[pallet::constant] + type PalletId: Get; + + #[pallet::constant] + type BitcoinCaip2: Get<&'static str>; + + #[pallet::constant] + type MpcRootSignerAddress: Get<[u8; 20]>; + + #[pallet::constant] + type VaultPubkeyHash: Get<[u8; 20]>; + + #[pallet::constant] + type KeyVersion: Get; + + type WeightInfo: crate::WeightInfo; + } + + #[pallet::storage] + #[pallet::getter(fn pallet_config)] + pub type PalletConfig = StorageValue<_, PalletConfigData, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn pending_deposits)] + pub type PendingDeposits = + StorageMap<_, Blake2_128Concat, Bytes32, PendingDepositData, OptionQuery>; + + #[pallet::storage] + pub type UsedRequestIds = StorageMap<_, Blake2_128Concat, Bytes32, (), OptionQuery>; + + #[pallet::storage] + pub type PendingWithdrawals = + StorageMap<_, Blake2_128Concat, Bytes32, PendingWithdrawalData, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn user_balances)] + pub type UserBalances = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Paused, + Unpaused, + DepositRequested { + request_id: Bytes32, + requester: T::AccountId, + amount_sats: u64, + txid: Bytes32, + }, + DepositClaimed { + request_id: Bytes32, + claimer: T::AccountId, + amount_sats: u64, + }, + WithdrawalRequested { + request_id: Bytes32, + requester: T::AccountId, + amount_sats: u64, + }, + WithdrawalCompleted { + request_id: Bytes32, + requester: T::AccountId, + amount_sats: u64, + }, + WithdrawalFailed { + request_id: Bytes32, + requester: T::AccountId, + amount_sats: u64, + }, + } + + #[pallet::error] + pub enum Error { + Paused, + DuplicateRequest, + InvalidRequestId, + DepositNotFound, + UnauthorizedClaimer, + InvalidSignature, + InvalidSigner, + Serialization, + InvalidOutput, + TransferFailed, + Overflow, + PathTooLong, + PalletAccountNotFunded, + NoVaultOutput, + InvalidVaultOutput, + SerializationError, + InsufficientBalance, + WithdrawalNotFound, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::pause())] + pub fn pause(origin: OriginFor) -> DispatchResult { + ::UpdateOrigin::ensure_origin(origin)?; + if PalletConfig::::get().is_none() { + PalletConfig::::put(PalletConfigData { paused: true }); + } else { + PalletConfig::::mutate_exists(|p| p.as_mut().unwrap().paused = true); + }; + Self::deposit_event(Event::Paused); + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::unpause())] + pub fn unpause(origin: OriginFor) -> DispatchResult { + ::UpdateOrigin::ensure_origin(origin)?; + if PalletConfig::::get().is_none() { + PalletConfig::::put(PalletConfigData { paused: false }); + } else { + PalletConfig::::mutate_exists(|p| p.as_mut().unwrap().paused = false); + }; + Self::deposit_event(Event::Unpaused); + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::request_deposit())] + pub fn request_deposit( + origin: OriginFor, + request_id: Bytes32, + inputs: BoundedVec, + outputs: BoundedVec, + lock_time: u32, + ) -> DispatchResult { + let requester = ensure_signed(origin)?; + Self::ensure_not_paused()?; + + ensure!( + UsedRequestIds::::get(request_id).is_none(), + Error::::DuplicateRequest + ); + ensure!( + PendingDeposits::::get(request_id).is_none(), + Error::::DuplicateRequest + ); + + let pallet_acc = Self::account_id(); + + let signet_deposit = pallet_signet::Pallet::::signature_deposit(); + let existential_deposit = ::Currency::minimum_balance(); + let pallet_balance = ::Currency::free_balance(&pallet_acc); + let required_balance = existential_deposit.saturating_add(signet_deposit); + ensure!(pallet_balance >= required_balance, Error::::PalletAccountNotFunded); + + let vault_script = Self::create_p2wpkh_script(&T::VaultPubkeyHash::get()); + let deposit_amount = outputs + .iter() + .find(|o| o.script_pubkey.to_vec() == vault_script) + .map(|o| o.value) + .ok_or(Error::::NoVaultOutput)?; + ensure!(deposit_amount > 0, Error::::InvalidVaultOutput); + + let path = Self::build_path(&requester); + + let psbt_bytes = pallet_signet::Pallet::::build_bitcoin_tx( + frame_system::RawOrigin::Signed(requester.clone()).into(), + inputs.clone(), + outputs.clone(), + lock_time, + )?; + + let txid = pallet_signet::Pallet::::get_txid( + frame_system::RawOrigin::Signed(requester.clone()).into(), + inputs, + outputs, + lock_time, + )?; + + let derived = Self::generate_request_id( + &pallet_acc, + txid.as_ref(), + T::BitcoinCaip2::get(), + T::KeyVersion::get(), + &path, + ECDSA, + BITCOIN, + b"", + ); + + ensure!(derived == request_id, Error::::InvalidRequestId); + + ::Currency::transfer( + &requester, + &pallet_acc, + signet_deposit, + frame_support::traits::ExistenceRequirement::AllowDeath, + )?; + + PendingDeposits::::insert( + request_id, + PendingDepositData { + requester: requester.clone(), + amount_sats: deposit_amount, + txid, + path: path.clone().try_into().map_err(|_| Error::::PathTooLong)?, + }, + ); + + let explorer_schema = + serde_json::to_vec(&serde_json::json!("bool")).map_err(|_| Error::::Serialization)?; + let callback_schema = + serde_json::to_vec(&serde_json::json!("bool")).map_err(|_| Error::::Serialization)?; + + pallet_signet::Pallet::::sign_bidirectional( + frame_system::RawOrigin::Signed(Self::account_id()).into(), + BoundedVec::try_from(psbt_bytes).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(T::BitcoinCaip2::get().as_bytes().to_vec()) + .map_err(|_| Error::::SerializationError)?, + 0, + BoundedVec::try_from(path).map_err(|_| Error::::PathTooLong)?, + BoundedVec::try_from(b"ecdsa".to_vec()).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(b"bitcoin".to_vec()).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(vec![]).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(explorer_schema).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(callback_schema).map_err(|_| Error::::SerializationError)?, + )?; + + UsedRequestIds::::insert(request_id, ()); + + Self::deposit_event(Event::DepositRequested { + request_id, + requester, + amount_sats: deposit_amount, + txid, + }); + + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::claim_deposit())] + pub fn claim_deposit( + origin: OriginFor, + request_id: Bytes32, + serialized_output: BoundedVec>, + signature: pallet_signet::Signature, + ) -> DispatchResult { + let claimer = ensure_signed(origin)?; + + let pending = PendingDeposits::::get(request_id).ok_or(Error::::DepositNotFound)?; + ensure!(pending.requester == claimer, Error::::UnauthorizedClaimer); + + #[cfg(not(any(feature = "runtime-benchmarks", test)))] + { + let msg_hash = Self::hash_message(&request_id, &serialized_output); + Self::verify_signature_from_address(&msg_hash, &signature, &T::MpcRootSignerAddress::get())?; + } + #[cfg(any(feature = "runtime-benchmarks", test))] + let _ = &signature; + + let success = Self::decode_success(&serialized_output)?; + ensure!(success, Error::::TransferFailed); + + UserBalances::::mutate(&claimer, |bal| -> DispatchResult { + *bal = bal.checked_add(pending.amount_sats).ok_or(Error::::Overflow)?; + Ok(()) + })?; + + PendingDeposits::::remove(request_id); + + Self::deposit_event(Event::DepositClaimed { + request_id, + claimer, + amount_sats: pending.amount_sats, + }); + + Ok(()) + } + + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::withdraw_btc())] + pub fn withdraw_btc( + origin: OriginFor, + request_id: Bytes32, + amount: u64, + recipient_script: BoundedVec, + inputs: BoundedVec, + outputs: BoundedVec, + lock_time: u32, + ) -> DispatchResult { + let requester = ensure_signed(origin)?; + Self::ensure_not_paused()?; + + ensure!( + UsedRequestIds::::get(request_id).is_none(), + Error::::DuplicateRequest + ); + ensure!( + PendingWithdrawals::::get(request_id).is_none(), + Error::::DuplicateRequest + ); + + let pallet_acc = Self::account_id(); + + let signet_deposit = pallet_signet::Pallet::::signature_deposit(); + let existential_deposit = ::Currency::minimum_balance(); + let pallet_balance = ::Currency::free_balance(&pallet_acc); + let required_balance = existential_deposit.saturating_add(signet_deposit); + ensure!(pallet_balance >= required_balance, Error::::PalletAccountNotFunded); + + // Verify recipient output exists in the transaction + let has_recipient = outputs + .iter() + .any(|o| o.script_pubkey.to_vec() == recipient_script.to_vec()); + ensure!(has_recipient, Error::::InvalidOutput); + + let txid = pallet_signet::Pallet::::get_txid( + frame_system::RawOrigin::Signed(requester.clone()).into(), + inputs.clone(), + outputs.clone(), + lock_time, + )?; + + let path = WITHDRAWAL_PATH; + + let derived = Self::generate_request_id( + &pallet_acc, + txid.as_ref(), + T::BitcoinCaip2::get(), + T::KeyVersion::get(), + path, + ECDSA, + BITCOIN, + b"", + ); + + ensure!(derived == request_id, Error::::InvalidRequestId); + + // Optimistically decrement balance + UserBalances::::mutate(&requester, |bal| -> DispatchResult { + *bal = bal.checked_sub(amount).ok_or(Error::::InsufficientBalance)?; + Ok(()) + })?; + + ::Currency::transfer( + &requester, + &pallet_acc, + signet_deposit, + frame_support::traits::ExistenceRequirement::AllowDeath, + )?; + + PendingWithdrawals::::insert( + request_id, + PendingWithdrawalData { + requester: requester.clone(), + amount_sats: amount, + }, + ); + + let psbt_bytes = pallet_signet::Pallet::::build_bitcoin_tx( + frame_system::RawOrigin::Signed(requester.clone()).into(), + inputs, + outputs, + lock_time, + )?; + + let explorer_schema = + serde_json::to_vec(&serde_json::json!("bool")).map_err(|_| Error::::Serialization)?; + let callback_schema = + serde_json::to_vec(&serde_json::json!("bool")).map_err(|_| Error::::Serialization)?; + + pallet_signet::Pallet::::sign_bidirectional( + frame_system::RawOrigin::Signed(Self::account_id()).into(), + BoundedVec::try_from(psbt_bytes).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(T::BitcoinCaip2::get().as_bytes().to_vec()) + .map_err(|_| Error::::SerializationError)?, + 0, + BoundedVec::try_from(path.to_vec()).map_err(|_| Error::::PathTooLong)?, + BoundedVec::try_from(b"ecdsa".to_vec()).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(b"bitcoin".to_vec()).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(vec![]).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(explorer_schema).map_err(|_| Error::::SerializationError)?, + BoundedVec::try_from(callback_schema).map_err(|_| Error::::SerializationError)?, + )?; + + UsedRequestIds::::insert(request_id, ()); + + Self::deposit_event(Event::WithdrawalRequested { + request_id, + requester, + amount_sats: amount, + }); + + Ok(()) + } + + #[pallet::call_index(7)] + #[pallet::weight(::WeightInfo::complete_withdraw_btc())] + pub fn complete_withdraw_btc( + origin: OriginFor, + request_id: Bytes32, + serialized_output: BoundedVec>, + signature: pallet_signet::Signature, + ) -> DispatchResult { + let caller = ensure_signed(origin)?; + + let pending = PendingWithdrawals::::get(request_id).ok_or(Error::::WithdrawalNotFound)?; + ensure!(pending.requester == caller, Error::::UnauthorizedClaimer); + + #[cfg(not(any(feature = "runtime-benchmarks", test)))] + { + let msg_hash = Self::hash_message(&request_id, &serialized_output); + Self::verify_signature_from_address(&msg_hash, &signature, &T::MpcRootSignerAddress::get())?; + } + #[cfg(any(feature = "runtime-benchmarks", test))] + let _ = &signature; + + let is_error = serialized_output.len() >= 4 && serialized_output[..4] == ERROR_PREFIX; + + if is_error { + // Refund the user + UserBalances::::mutate(&pending.requester, |bal| -> DispatchResult { + *bal = bal.checked_add(pending.amount_sats).ok_or(Error::::Overflow)?; + Ok(()) + })?; + + PendingWithdrawals::::remove(request_id); + + Self::deposit_event(Event::WithdrawalFailed { + request_id, + requester: pending.requester, + amount_sats: pending.amount_sats, + }); + } else { + let success = Self::decode_success(&serialized_output)?; + if success { + PendingWithdrawals::::remove(request_id); + + Self::deposit_event(Event::WithdrawalCompleted { + request_id, + requester: pending.requester, + amount_sats: pending.amount_sats, + }); + } else { + // BTC transaction failed, refund + UserBalances::::mutate(&pending.requester, |bal| -> DispatchResult { + *bal = bal.checked_add(pending.amount_sats).ok_or(Error::::Overflow)?; + Ok(()) + })?; + + PendingWithdrawals::::remove(request_id); + + Self::deposit_event(Event::WithdrawalFailed { + request_id, + requester: pending.requester, + amount_sats: pending.amount_sats, + }); + } + } + + Ok(()) + } + } + + impl Pallet { + #[inline] + pub(crate) fn ensure_not_paused() -> Result<(), Error> { + match PalletConfig::::get() { + Some(PalletConfigData { paused: true, .. }) => Err(Error::::Paused), + _ => Ok(()), + } + } + + fn build_path(who: &T::AccountId) -> Vec { + let enc = who.encode(); + let mut path = Vec::with_capacity(2 + enc.len() * 2); + path.extend_from_slice(b"0x"); + path.extend_from_slice(hex::encode(enc).as_bytes()); + path + } + + fn sender_ss58(sender: &T::AccountId) -> alloc::string::String { + use sp_core::crypto::Ss58Codec; + + let encoded = sender.encode(); + let mut bytes = [0u8; 32]; + let len = encoded.len().min(32); + bytes[..len].copy_from_slice(&encoded[..len]); + + let account_id32 = sp_runtime::AccountId32::from(bytes); + account_id32.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(0)) + } + + #[allow(clippy::too_many_arguments)] + pub fn generate_request_id( + sender: &T::AccountId, + transaction_data: &[u8], + caip2_id: &str, + key_version: u32, + path: &[u8], + algo: &[u8], + dest: &[u8], + params: &[u8], + ) -> Bytes32 { + use alloy_sol_types::SolValue; + + let sender_ss58 = Self::sender_ss58(sender); + + let encoded = ( + sender_ss58.as_str(), + transaction_data, + caip2_id, + key_version, + core::str::from_utf8(path).unwrap_or(""), + core::str::from_utf8(algo).unwrap_or(""), + core::str::from_utf8(dest).unwrap_or(""), + core::str::from_utf8(params).unwrap_or(""), + ) + .abi_encode_packed(); + + sp_io::hashing::keccak_256(&encoded) + } + + #[allow(dead_code)] + fn hash_message(request_id: &Bytes32, output: &[u8]) -> Bytes32 { + let mut data = Vec::with_capacity(32 + output.len()); + data.extend_from_slice(request_id); + data.extend_from_slice(output); + hashing::keccak_256(&data) + } + + #[allow(dead_code)] + fn verify_signature_from_address( + message_hash: &Bytes32, + signature: &pallet_signet::Signature, + expected_address: &[u8; 20], + ) -> DispatchResult { + ensure!(signature.recovery_id < 4, Error::::InvalidSignature); + + let mut sig_bytes = [0u8; 65]; + sig_bytes[..32].copy_from_slice(&signature.big_r.x); + sig_bytes[32..64].copy_from_slice(&signature.s); + sig_bytes[64] = signature.recovery_id; + + let pubkey = sp_io::crypto::secp256k1_ecdsa_recover(&sig_bytes, message_hash) + .map_err(|_| Error::::InvalidSignature)?; + + let pubkey_hash = hashing::keccak_256(&pubkey); + let recovered = &pubkey_hash[12..]; + + ensure!(recovered == expected_address, Error::::InvalidSigner); + Ok(()) + } + + pub(crate) fn create_p2wpkh_script(pubkey_hash: &[u8; 20]) -> Vec { + let mut script = Vec::with_capacity(22); + script.push(0x00); + script.push(0x14); + script.extend_from_slice(pubkey_hash); + script + } + + pub(crate) fn decode_success(out: &[u8]) -> Result> { + use borsh::BorshDeserialize; + bool::try_from_slice(out).map_err(|_| Error::::InvalidOutput) + } + + pub fn account_id() -> T::AccountId { + ::PalletId::get().into_account_truncating() + } + } +} diff --git a/pallets/btc-vault/src/tests/mod.rs b/pallets/btc-vault/src/tests/mod.rs new file mode 100644 index 0000000000..32584786bb --- /dev/null +++ b/pallets/btc-vault/src/tests/mod.rs @@ -0,0 +1,127 @@ +pub mod test_cases; + +use crate as pallet_btc_vault; +use frame_support::{ + parameter_types, + traits::{Currency, Everything}, + PalletId, +}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Signet: pallet_signet, + BtcVault: pallet_btc_vault, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const ExistentialDeposit: u128 = 1; + pub const SignetPalletId: PalletId = PalletId(*b"py/signt"); + pub const BtcVaultPalletId: PalletId = PalletId(*b"btcvault"); + pub const MaxChainIdLength: u32 = 128; + pub const MaxDataLength: u32 = 100_000; + pub const MaxSignatureDeposit: u32 = 10_000_000; + pub const MaxInputs: u32 = 10; + pub const MaxOutputs: u32 = 10; + pub const BitcoinCaip2: &'static str = "bip122:000000000019d6689c085ae165831e93"; + pub const MpcRootSignerAddress: [u8; 20] = [1u8; 20]; + pub const VaultPubkeyHash: [u8; 20] = [0xAA; 20]; + pub const KeyVersion: u32 = 0; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type RuntimeTask = (); + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +impl pallet_balances::Config for Test { + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type DoneSlashHandler = (); +} + +impl pallet_signet::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type PalletId = SignetPalletId; + type MaxChainIdLength = MaxChainIdLength; + type WeightInfo = pallet_signet::weights::WeightInfo; + type MaxDataLength = MaxDataLength; + type UpdateOrigin = frame_system::EnsureRoot; + type MaxSignatureDeposit = MaxSignatureDeposit; + type MaxInputs = MaxInputs; + type MaxOutputs = MaxOutputs; +} + +impl pallet_btc_vault::Config for Test { + type RuntimeEvent = RuntimeEvent; + type UpdateOrigin = frame_system::EnsureRoot; + type PalletId = BtcVaultPalletId; + type BitcoinCaip2 = BitcoinCaip2; + type MpcRootSignerAddress = MpcRootSignerAddress; + type VaultPubkeyHash = VaultPubkeyHash; + type KeyVersion = KeyVersion; + type WeightInfo = crate::weights::SubstrateWeight; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + let _ = Balances::deposit_creating(&1, 1_000_000); + let _ = Balances::deposit_creating(&2, 1_000_000); + let _ = Balances::deposit_creating(&3, 100); + }); + ext +} diff --git a/pallets/btc-vault/src/tests/test_cases.rs b/pallets/btc-vault/src/tests/test_cases.rs new file mode 100644 index 0000000000..861cf18f30 --- /dev/null +++ b/pallets/btc-vault/src/tests/test_cases.rs @@ -0,0 +1,393 @@ +use super::*; +use frame_support::{assert_noop, assert_ok, BoundedVec}; + +#[test] +fn pause_works() { + new_test_ext().execute_with(|| { + assert_ok!(BtcVault::pause(RuntimeOrigin::root())); + assert!(BtcVault::pallet_config().unwrap().paused); + System::assert_last_event(crate::Event::::Paused.into()); + }); +} + +#[test] +fn pause_requires_root() { + new_test_ext().execute_with(|| { + assert_noop!( + BtcVault::pause(RuntimeOrigin::signed(1)), + frame_support::error::BadOrigin + ); + }); +} + +#[test] +fn unpause_works() { + new_test_ext().execute_with(|| { + assert_ok!(BtcVault::pause(RuntimeOrigin::root())); + assert!(BtcVault::pallet_config().unwrap().paused); + + assert_ok!(BtcVault::unpause(RuntimeOrigin::root())); + assert!(!BtcVault::pallet_config().unwrap().paused); + System::assert_last_event(crate::Event::::Unpaused.into()); + }); +} + +#[test] +fn unpause_requires_root() { + new_test_ext().execute_with(|| { + assert_noop!( + BtcVault::unpause(RuntimeOrigin::signed(1)), + frame_support::error::BadOrigin + ); + }); +} + +fn insert_pending_deposit(request_id: crate::Bytes32, requester: u64, amount_sats: u64) { + crate::PendingDeposits::::insert( + request_id, + crate::PendingDepositData { + requester, + amount_sats, + txid: [1u8; 32], + path: BoundedVec::try_from(b"0xdeadbeef".to_vec()).unwrap(), + }, + ); +} + +fn dummy_signature() -> pallet_signet::Signature { + pallet_signet::Signature { + big_r: pallet_signet::AffinePoint { + x: [0u8; 32], + y: [0u8; 32], + }, + s: [0u8; 32], + recovery_id: 0, + } +} + +#[test] +fn claim_deposit_works() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + let amount = 50_000u64; + insert_pending_deposit(request_id, 1, amount); + + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_ok!(BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + )); + + assert_eq!(BtcVault::user_balances(1), amount); + assert!(BtcVault::pending_deposits(request_id).is_none()); + System::assert_last_event( + crate::Event::::DepositClaimed { + request_id, + claimer: 1, + amount_sats: amount, + } + .into(), + ); + }); +} + +#[test] +fn claim_deposit_accumulates_balance() { + new_test_ext().execute_with(|| { + let id1 = [1u8; 32]; + let id2 = [2u8; 32]; + insert_pending_deposit(id1, 1, 30_000); + insert_pending_deposit(id2, 1, 20_000); + + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_ok!(BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + id1, + output.clone(), + dummy_signature(), + )); + assert_eq!(BtcVault::user_balances(1), 30_000); + + assert_ok!(BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + id2, + output, + dummy_signature(), + )); + assert_eq!(BtcVault::user_balances(1), 50_000); + }); +} + +#[test] +fn claim_deposit_fails_not_found() { + new_test_ext().execute_with(|| { + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_noop!( + BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + [99u8; 32], + output, + dummy_signature(), + ), + crate::Error::::DepositNotFound + ); + }); +} + +#[test] +fn claim_deposit_fails_unauthorized_claimer() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + insert_pending_deposit(request_id, 1, 50_000); + + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_noop!( + BtcVault::claim_deposit( + RuntimeOrigin::signed(2), + request_id, + output, + dummy_signature(), + ), + crate::Error::::UnauthorizedClaimer + ); + }); +} + +#[test] +fn claim_deposit_fails_on_false_output() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + insert_pending_deposit(request_id, 1, 50_000); + + let output: BoundedVec> = + BoundedVec::try_from(vec![0u8]).unwrap(); + + assert_noop!( + BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + ), + crate::Error::::TransferFailed + ); + }); +} + +#[test] +fn claim_deposit_fails_on_invalid_output() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + insert_pending_deposit(request_id, 1, 50_000); + + let output: BoundedVec> = + BoundedVec::try_from(vec![2u8]).unwrap(); + + assert_noop!( + BtcVault::claim_deposit( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + ), + crate::Error::::InvalidOutput + ); + }); +} + +#[test] +fn create_p2wpkh_script_is_correct() { + new_test_ext().execute_with(|| { + let pubkey_hash = [0xAA; 20]; + let script = crate::Pallet::::create_p2wpkh_script(&pubkey_hash); + assert_eq!(script.len(), 22); + assert_eq!(script[0], 0x00); + assert_eq!(script[1], 0x14); + assert_eq!(&script[2..], &pubkey_hash); + }); +} + +#[test] +fn decode_success_works() { + new_test_ext().execute_with(|| { + assert_eq!(crate::Pallet::::decode_success(&[1u8]).unwrap(), true); + assert_eq!(crate::Pallet::::decode_success(&[0u8]).unwrap(), false); + assert!(crate::Pallet::::decode_success(&[2u8]).is_err()); + assert!(crate::Pallet::::decode_success(&[]).is_err()); + }); +} + +#[test] +fn ensure_not_paused_works() { + new_test_ext().execute_with(|| { + assert!(crate::Pallet::::ensure_not_paused().is_ok()); + + assert_ok!(BtcVault::pause(RuntimeOrigin::root())); + assert!(crate::Pallet::::ensure_not_paused().is_err()); + + assert_ok!(BtcVault::unpause(RuntimeOrigin::root())); + assert!(crate::Pallet::::ensure_not_paused().is_ok()); + }); +} + +// ========= Withdrawal tests ========= + +fn insert_pending_withdrawal(request_id: crate::Bytes32, requester: u64, amount_sats: u64) { + crate::PendingWithdrawals::::insert( + request_id, + crate::PendingWithdrawalData { + requester, + amount_sats, + }, + ); +} + +#[test] +fn complete_withdraw_btc_success() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + let amount = 50_000u64; + crate::UserBalances::::insert(1u64, 100_000u64); + insert_pending_withdrawal(request_id, 1, amount); + + // borsh-encoded true + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_ok!(BtcVault::complete_withdraw_btc( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + )); + + // Balance should remain unchanged (already decremented at withdrawal time) + assert_eq!(BtcVault::user_balances(1), 100_000); + assert!(crate::PendingWithdrawals::::get(request_id).is_none()); + System::assert_last_event( + crate::Event::::WithdrawalCompleted { + request_id, + requester: 1, + amount_sats: amount, + } + .into(), + ); + }); +} + +#[test] +fn complete_withdraw_btc_refund_on_error() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + let amount = 50_000u64; + let initial_balance = 100_000u64; + crate::UserBalances::::insert(1u64, initial_balance); + insert_pending_withdrawal(request_id, 1, amount); + + // Error prefix output + let mut error_output = vec![0xde, 0xad, 0xbe, 0xef]; + error_output.extend_from_slice(b"some error message"); + let output: BoundedVec> = + BoundedVec::try_from(error_output).unwrap(); + + assert_ok!(BtcVault::complete_withdraw_btc( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + )); + + // Balance should be refunded + assert_eq!(BtcVault::user_balances(1), initial_balance + amount); + assert!(crate::PendingWithdrawals::::get(request_id).is_none()); + System::assert_last_event( + crate::Event::::WithdrawalFailed { + request_id, + requester: 1, + amount_sats: amount, + } + .into(), + ); + }); +} + +#[test] +fn complete_withdraw_btc_refund_on_false() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + let amount = 50_000u64; + let initial_balance = 100_000u64; + crate::UserBalances::::insert(1u64, initial_balance); + insert_pending_withdrawal(request_id, 1, amount); + + // borsh-encoded false + let output: BoundedVec> = + BoundedVec::try_from(vec![0u8]).unwrap(); + + assert_ok!(BtcVault::complete_withdraw_btc( + RuntimeOrigin::signed(1), + request_id, + output, + dummy_signature(), + )); + + assert_eq!(BtcVault::user_balances(1), initial_balance + amount); + System::assert_last_event( + crate::Event::::WithdrawalFailed { + request_id, + requester: 1, + amount_sats: amount, + } + .into(), + ); + }); +} + +#[test] +fn complete_withdraw_btc_fails_not_found() { + new_test_ext().execute_with(|| { + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_noop!( + BtcVault::complete_withdraw_btc( + RuntimeOrigin::signed(1), + [99u8; 32], + output, + dummy_signature(), + ), + crate::Error::::WithdrawalNotFound + ); + }); +} + +#[test] +fn complete_withdraw_btc_fails_unauthorized() { + new_test_ext().execute_with(|| { + let request_id = [42u8; 32]; + insert_pending_withdrawal(request_id, 1, 50_000); + + let output: BoundedVec> = + BoundedVec::try_from(vec![1u8]).unwrap(); + + assert_noop!( + BtcVault::complete_withdraw_btc( + RuntimeOrigin::signed(2), + request_id, + output, + dummy_signature(), + ), + crate::Error::::UnauthorizedClaimer + ); + }); +} diff --git a/pallets/btc-vault/src/types.rs b/pallets/btc-vault/src/types.rs new file mode 100644 index 0000000000..fecc70d1df --- /dev/null +++ b/pallets/btc-vault/src/types.rs @@ -0,0 +1,36 @@ +use super::*; + +pub type Bytes32 = [u8; 32]; + +pub const MAX_SERIALIZED_OUTPUT_LENGTH: u32 = 65_536; + +pub const ECDSA: &[u8] = b"ecdsa"; +pub const BITCOIN: &[u8] = b"bitcoin"; + +#[derive(Encode, Decode, TypeInfo, Clone, Debug, PartialEq, MaxEncodedLen)] +pub struct PalletConfigData { + pub paused: bool, +} + +impl Default for PalletConfigData { + fn default() -> Self { + Self { paused: false } + } +} + +#[derive(Encode, Decode, TypeInfo, Clone, Debug, PartialEq, MaxEncodedLen)] +pub struct PendingDepositData { + pub requester: AccountId, + pub amount_sats: u64, + pub txid: Bytes32, + pub path: BoundedVec>, +} + +#[derive(Encode, Decode, TypeInfo, Clone, Debug, PartialEq, MaxEncodedLen)] +pub struct PendingWithdrawalData { + pub requester: AccountId, + pub amount_sats: u64, +} + +pub const ERROR_PREFIX: [u8; 4] = [0xde, 0xad, 0xbe, 0xef]; +pub const WITHDRAWAL_PATH: &[u8] = b"root"; diff --git a/pallets/btc-vault/src/weights.rs b/pallets/btc-vault/src/weights.rs new file mode 100644 index 0000000000..2858ee363f --- /dev/null +++ b/pallets/btc-vault/src/weights.rs @@ -0,0 +1,74 @@ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +pub trait WeightInfo { + fn pause() -> Weight; + fn unpause() -> Weight; + fn request_deposit() -> Weight; + fn claim_deposit() -> Weight; + fn withdraw_btc() -> Weight; + fn complete_withdraw_btc() -> Weight; +} + +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn pause() -> Weight { + Weight::from_parts(9_000_000, 0) + .saturating_add(Weight::from_parts(0, 1486)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + fn unpause() -> Weight { + Weight::from_parts(9_000_000, 0) + .saturating_add(Weight::from_parts(0, 1486)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + fn request_deposit() -> Weight { + Weight::from_parts(150_000_000, 65536) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } + fn claim_deposit() -> Weight { + Weight::from_parts(60_000_000, 4096) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } + fn withdraw_btc() -> Weight { + Weight::from_parts(150_000_000, 65536) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + } + fn complete_withdraw_btc() -> Weight { + Weight::from_parts(60_000_000, 4096) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} + +impl WeightInfo for () { + fn pause() -> Weight { + Weight::from_parts(9_000_000, 0) + } + fn unpause() -> Weight { + Weight::from_parts(9_000_000, 0) + } + fn request_deposit() -> Weight { + Weight::from_parts(150_000_000, 0) + } + fn claim_deposit() -> Weight { + Weight::from_parts(60_000_000, 0) + } + fn withdraw_btc() -> Weight { + Weight::from_parts(150_000_000, 0) + } + fn complete_withdraw_btc() -> Weight { + Weight::from_parts(60_000_000, 0) + } +} diff --git a/pallets/dispenser/Cargo.toml b/pallets/dispenser/Cargo.toml index 7e38ce21a0..511b60dd69 100644 --- a/pallets/dispenser/Cargo.toml +++ b/pallets/dispenser/Cargo.toml @@ -57,8 +57,8 @@ std = [ "serde_json/std", "pallet-currencies/std", "pallet-signet/std", + "pallet-asset-registry/std", "hex/std", - ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -67,8 +67,7 @@ runtime-benchmarks = [ ] try-runtime = ["frame-support/try-runtime"] -[target.'cfg(not(feature = "std"))'.dependencies] -borsh = { version = "1.5", default-features = false, features = ["derive", "hashbrown"] } - -[target.'cfg(feature = "std")'.dependencies] -borsh = { version = "1.5", default-features = false, features = ["derive", "std"] } +[dependencies.borsh] +version = "1.5" +default-features = false +features = ["derive", "hashbrown"] diff --git a/pallets/dispenser/src/lib.rs b/pallets/dispenser/src/lib.rs index 242f24b20c..66be20f82a 100644 --- a/pallets/dispenser/src/lib.rs +++ b/pallets/dispenser/src/lib.rs @@ -82,6 +82,8 @@ pub mod pallet { /// Multi-asset fungible currency implementation used for fees and faucet tokens. type Currency: Mutate; + type UpdateOrigin: EnsureOrigin; + /// Minimum amount of faucet asset that can be requested in a single call. #[pallet::constant] type MinimumRequestAmount: Get; @@ -370,7 +372,7 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight(::WeightInfo::pause())] pub fn pause(origin: OriginFor) -> DispatchResult { - T::UpdateOrigin::ensure_origin(origin)?; + ::UpdateOrigin::ensure_origin(origin)?; if DispenserConfig::::get().is_none() { DispenserConfig::::put(DispenserConfigData { paused: true }); } else { @@ -388,7 +390,7 @@ pub mod pallet { #[pallet::call_index(3)] #[pallet::weight(::WeightInfo::unpause())] pub fn unpause(origin: OriginFor) -> DispatchResult { - T::UpdateOrigin::ensure_origin(origin)?; + ::UpdateOrigin::ensure_origin(origin)?; if DispenserConfig::::get().is_none() { DispenserConfig::::put(DispenserConfigData { paused: false }); } else { @@ -409,7 +411,7 @@ pub mod pallet { #[pallet::call_index(4)] #[pallet::weight(::WeightInfo::set_faucet_balance())] pub fn set_faucet_balance(origin: OriginFor, balance_wei: Balance) -> DispatchResult { - T::UpdateOrigin::ensure_origin(origin)?; + ::UpdateOrigin::ensure_origin(origin)?; let old = FaucetBalanceWei::::get(); let new_balance = old + balance_wei; FaucetBalanceWei::::put(new_balance); diff --git a/pallets/dispenser/src/tests/mod.rs b/pallets/dispenser/src/tests/mod.rs index 02dbaba9bb..c93a7b56d4 100644 --- a/pallets/dispenser/src/tests/mod.rs +++ b/pallets/dispenser/src/tests/mod.rs @@ -146,6 +146,11 @@ parameter_types! { pub const MaxSignatureDeposit: u128 = 100_000_000_000; } +parameter_types! { + pub const MaxInputs: u32 = 10; + pub const MaxOutputs: u32 = 10; +} + impl pallet_signet::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -155,6 +160,8 @@ impl pallet_signet::Config for Test { type MaxDataLength = MaxDataLength; type UpdateOrigin = frame_system::EnsureRoot; type MaxSignatureDeposit = MaxSignatureDeposit; + type MaxInputs = MaxInputs; + type MaxOutputs = MaxOutputs; } parameter_types! { @@ -179,6 +186,7 @@ impl pallet_dispenser::Config for Test { type RuntimeEvent = RuntimeEvent; type PalletId = DispenserPalletId; type Currency = FungibleCurrencies; + type UpdateOrigin = frame_system::EnsureRoot; type MinimumRequestAmount = SigEthFaucetMinRequest; type MaxDispenseAmount = SigEthFaucetMaxDispense; type DispenserFee = SigEthFaucetDispenserFee; diff --git a/pallets/signet/Cargo.toml b/pallets/signet/Cargo.toml index 470aeba4b9..0462e3fa7e 100644 --- a/pallets/signet/Cargo.toml +++ b/pallets/signet/Cargo.toml @@ -25,6 +25,7 @@ sp-io = { workspace = true, optional = true } sp-core = { workspace = true } rlp = { version = "0.6", default-features = false } +signet-rs = { git = "https://github.com/esaminu/signet.rs", branch = "borsh-1.5.7", default-features = false, features = ["bitcoin"] } [dev-dependencies] sp-io = { workspace = true } diff --git a/pallets/signet/src/lib.rs b/pallets/signet/src/lib.rs index 6a9d816279..b1be712509 100644 --- a/pallets/signet/src/lib.rs +++ b/pallets/signet/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] +use crate::types::MaxScriptLength; use ethereum::{AccessListItem, EIP1559TransactionMessage, TransactionAction}; use frame_support::{ pallet_prelude::*, @@ -77,6 +78,11 @@ pub mod pallet { #[pallet::constant] type MaxSignatureDeposit: Get>; + + #[pallet::constant] + type MaxInputs: Get; + #[pallet::constant] + type MaxOutputs: Get; } // ======================================== @@ -112,6 +118,21 @@ pub mod pallet { pub error_message: BoundedVec>, } + #[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug)] + pub struct UtxoInput { + pub txid: [u8; 32], + pub vout: u32, + pub value: u64, + pub script_pubkey: BoundedVec, + pub sequence: u32, + } + + #[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug)] + pub struct BitcoinOutput { + pub value: u64, + pub script_pubkey: BoundedVec, + } + // ======================================== // Storage // ======================================== @@ -236,6 +257,12 @@ pub mod pallet { InvalidGasPrice, /// Signature Deposit cannot exceed MaxSignatureDeposit MaxDepositExceeded, + + NoInputs, + NoOutputs, + InvalidLockTime, + PsbtCreationFailed, + PsbtSerializationFailed, } // ======================================== @@ -539,5 +566,95 @@ pub mod pallet { Ok(output) } + + pub fn build_bitcoin_tx( + origin: OriginFor, + inputs: BoundedVec, + outputs: BoundedVec, + lock_time: u32, + ) -> Result, DispatchError> { + ensure_signed(origin)?; + + ensure!(!inputs.is_empty(), Error::::NoInputs); + ensure!(!outputs.is_empty(), Error::::NoOutputs); + + let (psbt_bytes, _) = Self::build_psbt(&inputs, &outputs, lock_time)?; + Ok(psbt_bytes) + } + + pub fn get_txid( + origin: OriginFor, + inputs: BoundedVec, + outputs: BoundedVec, + lock_time: u32, + ) -> Result<[u8; 32], DispatchError> { + ensure_signed(origin)?; + + ensure!(!inputs.is_empty(), Error::::NoInputs); + ensure!(!outputs.is_empty(), Error::::NoOutputs); + + let (_, txid) = Self::build_psbt(&inputs, &outputs, lock_time)?; + Ok(txid) + } + + fn build_psbt( + inputs: &[UtxoInput], + outputs: &[BitcoinOutput], + lock_time: u32, + ) -> Result<(Vec, [u8; 32]), DispatchError> { + use signet_rs::bitcoin::types::{ + Amount, Hash, LockTime, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid, Version, Witness, + }; + use signet_rs::{TransactionBuilder, TxBuilder, BITCOIN}; + + let tx_inputs: Vec = inputs + .iter() + .map(|input| TxIn { + previous_output: OutPoint::new(Txid(Hash(input.txid)), input.vout), + script_sig: ScriptBuf::default(), + sequence: Sequence(input.sequence), + witness: Witness::default(), + }) + .collect(); + + let tx_outputs: Vec = outputs + .iter() + .map(|output| TxOut { + value: Amount::from_sat(output.value), + script_pubkey: ScriptBuf(output.script_pubkey.to_vec()), + }) + .collect(); + + let lock_time_parsed = if lock_time < 500_000_000 { + LockTime::from_height(lock_time).map_err(|_| Error::::InvalidLockTime)? + } else { + LockTime::from_time(lock_time).map_err(|_| Error::::InvalidLockTime)? + }; + + let tx = TransactionBuilder::new::() + .version(Version::Two) + .inputs(tx_inputs) + .outputs(tx_outputs) + .lock_time(lock_time_parsed) + .build(); + + let txid = tx.compute_txid(); + let mut txid_bytes = txid.as_byte_array(); + txid_bytes.reverse(); + + let mut psbt = tx.to_psbt(); + + for (i, input) in inputs.iter().enumerate() { + psbt.update_input_with_witness_utxo(i, input.script_pubkey.to_vec(), input.value) + .map_err(|_| Error::::PsbtCreationFailed)?; + + psbt.update_input_with_sighash_type(i, 1) + .map_err(|_| Error::::PsbtCreationFailed)?; + } + + let psbt_bytes = psbt.serialize().map_err(|_| Error::::PsbtSerializationFailed)?; + + Ok((psbt_bytes, txid_bytes)) + } } } diff --git a/pallets/signet/src/tests/mod.rs b/pallets/signet/src/tests/mod.rs index 4b2289f311..9333a2d8f8 100644 --- a/pallets/signet/src/tests/mod.rs +++ b/pallets/signet/src/tests/mod.rs @@ -136,6 +136,11 @@ parameter_types! { pub const MaxSignatureDeposit: u32 = 10000000; } +parameter_types! { + pub const MaxInputs: u32 = 10; + pub const MaxOutputs: u32 = 10; +} + impl pallet_signet::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -145,6 +150,8 @@ impl pallet_signet::Config for Test { type MaxDataLength = MaxDataLength; type UpdateOrigin = frame_system::EnsureRoot; type MaxSignatureDeposit = MaxSignatureDeposit; + type MaxInputs = MaxInputs; + type MaxOutputs = MaxOutputs; } parameter_types! { diff --git a/pallets/signet/src/tests/test_cases.rs b/pallets/signet/src/tests/test_cases.rs index fc777837ea..2573e495c0 100644 --- a/pallets/signet/src/tests/test_cases.rs +++ b/pallets/signet/src/tests/test_cases.rs @@ -1,13 +1,17 @@ use crate::{ tests::{ new_test_ext, - utils::{bounded_array, bounded_chain_id, bounded_err, bounded_sig, bounded_u8, create_test_signature}, - Balances, MockCaller, MockCallerPalletId, RuntimeEvent, RuntimeOrigin, Signet, System, Test, + utils::{ + bounded_array, bounded_chain_id, bounded_err, bounded_sig, bounded_u8, + create_test_bitcoin_output, create_test_signature, create_test_utxo_input, + }, + Balances, MaxInputs, MaxOutputs, MockCaller, MockCallerPalletId, RuntimeEvent, RuntimeOrigin, + Signet, System, Test, }, - Error, ErrorResponse, Event, + BitcoinOutput, Error, ErrorResponse, Event, UtxoInput, }; use frame_support::traits::Currency; -use frame_support::{assert_noop, assert_ok}; +use frame_support::{assert_noop, assert_ok, BoundedVec}; use sp_runtime::traits::AccountIdConversion; // ----------------------------------------------------------------------------- @@ -775,3 +779,101 @@ fn test_cross_pallet_execution() { println!(" NOT as: {} (the original user)", NON_ADMIN); }); } + +#[test] +fn test_build_bitcoin_tx_works() { + new_test_ext().execute_with(|| { + let inputs = BoundedVec::::try_from(vec![create_test_utxo_input( + [0x42; 32], + 0, + 100_000_000, + )]) + .unwrap(); + + let outputs = + BoundedVec::::try_from(vec![create_test_bitcoin_output(99_900_000)]) + .unwrap(); + + let result = Signet::build_bitcoin_tx(RuntimeOrigin::signed(ADMIN), inputs, outputs, 0); + + assert_ok!(&result); + let psbt = result.unwrap(); + assert!(!psbt.is_empty(), "PSBT should not be empty"); + }); +} + +#[test] +fn test_get_txid_works() { + new_test_ext().execute_with(|| { + let inputs = BoundedVec::::try_from(vec![create_test_utxo_input( + [0x42; 32], + 0, + 100_000_000, + )]) + .unwrap(); + + let outputs = + BoundedVec::::try_from(vec![create_test_bitcoin_output(99_900_000)]) + .unwrap(); + + let result = Signet::get_txid(RuntimeOrigin::signed(ADMIN), inputs.clone(), outputs.clone(), 0); + + assert_ok!(&result); + let txid = result.unwrap(); + assert_eq!(txid.len(), 32, "Txid should be 32 bytes"); + + let result2 = Signet::get_txid(RuntimeOrigin::signed(ADMIN), inputs, outputs, 0); + assert_eq!(txid, result2.unwrap(), "Same inputs should produce same txid"); + }); +} + +#[test] +fn test_build_bitcoin_tx_no_inputs_fails() { + new_test_ext().execute_with(|| { + let outputs = + BoundedVec::::try_from(vec![create_test_bitcoin_output(100_000_000)]) + .unwrap(); + + assert_noop!( + Signet::build_bitcoin_tx( + RuntimeOrigin::signed(ADMIN), + BoundedVec::default(), + outputs.clone(), + 0 + ), + Error::::NoInputs + ); + + assert_noop!( + Signet::get_txid(RuntimeOrigin::signed(ADMIN), BoundedVec::default(), outputs, 0), + Error::::NoInputs + ); + }); +} + +#[test] +fn test_build_bitcoin_tx_no_outputs_fails() { + new_test_ext().execute_with(|| { + let inputs = BoundedVec::::try_from(vec![create_test_utxo_input( + [0x42; 32], + 0, + 100_000_000, + )]) + .unwrap(); + + assert_noop!( + Signet::build_bitcoin_tx( + RuntimeOrigin::signed(ADMIN), + inputs.clone(), + BoundedVec::default(), + 0 + ), + Error::::NoOutputs + ); + + assert_noop!( + Signet::get_txid(RuntimeOrigin::signed(ADMIN), inputs, BoundedVec::default(), 0), + Error::::NoOutputs + ); + }); +} diff --git a/pallets/signet/src/tests/utils.rs b/pallets/signet/src/tests/utils.rs index 2183d0f28f..30bba15600 100644 --- a/pallets/signet/src/tests/utils.rs +++ b/pallets/signet/src/tests/utils.rs @@ -1,4 +1,4 @@ -use crate::{tests::MaxChainIdLength, AffinePoint, ErrorResponse, Signature}; +use crate::{tests::MaxChainIdLength, AffinePoint, BitcoinOutput, ErrorResponse, MaxScriptLength, Signature, UtxoInput}; use sp_core::ConstU32; use sp_runtime::BoundedVec; @@ -32,3 +32,20 @@ pub fn create_test_signature() -> Signature { recovery_id: 0, } } + +pub fn create_test_utxo_input(txid: [u8; 32], vout: u32, value: u64) -> UtxoInput { + UtxoInput { + txid, + vout, + value, + script_pubkey: BoundedVec::::try_from(vec![0x00, 0x14]).unwrap(), + sequence: 0xFFFFFFFF, + } +} + +pub fn create_test_bitcoin_output(value: u64) -> BitcoinOutput { + BitcoinOutput { + value, + script_pubkey: BoundedVec::::try_from(vec![0x00, 0x14]).unwrap(), + } +} diff --git a/pallets/signet/src/types.rs b/pallets/signet/src/types.rs index 836cde9945..907bdc80cc 100644 --- a/pallets/signet/src/types.rs +++ b/pallets/signet/src/types.rs @@ -1,3 +1,4 @@ +use frame_support::pallet_prelude::ConstU32; use frame_support::weights::Weight; pub trait WeightInfo { @@ -10,3 +11,5 @@ pub trait WeightInfo { fn respond_error() -> Weight; fn respond_bidirectional() -> Weight; } + +pub type MaxScriptLength = ConstU32<520>; diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 90b29d8182..0a465ff6ab 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -93,6 +93,7 @@ pallet-message-queue = { workspace = true } pallet-state-trie-migration = { workspace = true } pallet-signet = { workspace = true } pallet-dispenser = { workspace = true } +pallet-btc-vault = { workspace = true } # ORML dependencies orml-tokens = { workspace = true } @@ -263,6 +264,7 @@ runtime-benchmarks = [ "pallet-xcm-benchmarks/runtime-benchmarks", "pallet-signet/runtime-benchmarks", "pallet-dispenser/runtime-benchmarks", + "pallet-btc-vault/runtime-benchmarks", "ismp-parachain/runtime-benchmarks", "pallet-token-gateway/runtime-benchmarks", "cumulus-pallet-weight-reclaim/runtime-benchmarks", @@ -393,6 +395,7 @@ std = [ "pallet-dispatcher/std", "pallet-signet/std", "pallet-dispenser/std", + "pallet-btc-vault/std", # Hyperbridge "anyhow/std", "pallet-hyperbridge/std", diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 62295ef6be..9ea076dc70 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -1869,6 +1869,8 @@ parameter_types! { pub const MaxEvmDataLength: u32 = 100_000; pub const MaxSignatureDeposit: Balance = 200_000_000_000_000; + pub const MaxInputs: u32 = 256; + pub const MaxOutputs: u32 = 256; } impl pallet_signet::Config for Runtime { @@ -1880,6 +1882,8 @@ impl pallet_signet::Config for Runtime { type MaxDataLength = MaxEvmDataLength; type UpdateOrigin = EnsureRoot; type MaxSignatureDeposit = MaxSignatureDeposit; + type MaxInputs = MaxInputs; + type MaxOutputs = MaxOutputs; } parameter_types! { @@ -1911,6 +1915,7 @@ impl frame_support::traits::Get for SigEthFaucetContractAddr { impl pallet_dispenser::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = FungibleCurrencies; + type UpdateOrigin = EnsureRoot; type MinimumRequestAmount = SigEthFaucetMinRequest; type MaxDispenseAmount = SigEthFaucetMaxDispense; type DispenserFee = SigEthFaucetDispenserFee; @@ -1923,6 +1928,32 @@ impl pallet_dispenser::Config for Runtime { type WeightInfo = weights::pallet_dispenser::HydraWeight; } +parameter_types! { + pub const BtcVaultPalletId: PalletId = PalletId(*b"py/btcvt"); + pub const BitcoinCaip2: &'static str = "bip122:000000000933ea01ad0ee984209779ba"; + pub const MpcRootSignerAddress: [u8; 20] = [ + 0x1b, 0xe3, 0x1a, 0x94, 0x36, 0x1a, 0x39, 0x1b, 0xba, 0xfb, + 0x2a, 0x4c, 0xcd, 0x70, 0x4f, 0x57, 0xdc, 0x04, 0xd4, 0xbb, + ]; + // hash160 of compressed vault pubkey derived from MPC root key with path "root" + pub const VaultPubkeyHash: [u8; 20] = [ + 0x73, 0x79, 0xa7, 0x34, 0x07, 0x6e, 0xb8, 0x2b, 0xa7, 0xa2, + 0xf1, 0x8b, 0x4a, 0x0c, 0x7e, 0xc0, 0x9c, 0x64, 0x12, 0x67, + ]; + pub const KeyVersion: u32 = 0; +} + +impl pallet_btc_vault::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type UpdateOrigin = EnsureRoot; + type PalletId = BtcVaultPalletId; + type BitcoinCaip2 = BitcoinCaip2; + type MpcRootSignerAddress = MpcRootSignerAddress; + type VaultPubkeyHash = VaultPubkeyHash; + type KeyVersion = KeyVersion; + type WeightInfo = weights::pallet_btc_vault::HydraWeight; +} + pub struct ConvertViaOmnipool(PhantomData); impl Convert for ConvertViaOmnipool where diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 5e4638e968..64ea3aeef4 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -200,6 +200,7 @@ construct_runtime!( Parameters: pallet_parameters = 83, Signet: pallet_signet = 84, EthDispenser: pallet_dispenser = 85, + BtcVault: pallet_btc_vault = 86, // ORML related modules Tokens: orml_tokens = 77, @@ -363,6 +364,7 @@ mod benches { [pallet_dynamic_fees, DynamicFees] [pallet_signet, Signet] [pallet_dispenser, EthDispenser] + [pallet_btc_vault, BtcVault] [ismp_parachain, IsmpParachain] [pallet_token_gateway, TokenGateway] [frame_system_extensions, frame_system_benchmarking::extensions::Pallet::] diff --git a/runtime/hydradx/src/weights/mod.rs b/runtime/hydradx/src/weights/mod.rs index 9cae8c2744..3919641829 100644 --- a/runtime/hydradx/src/weights/mod.rs +++ b/runtime/hydradx/src/weights/mod.rs @@ -19,6 +19,7 @@ pub mod pallet_currencies; pub mod pallet_dca; pub mod pallet_democracy; pub mod pallet_dispatcher; +pub mod pallet_btc_vault; pub mod pallet_dispenser; pub mod pallet_duster; pub mod pallet_dynamic_evm_fee; diff --git a/runtime/hydradx/src/weights/pallet_btc_vault.rs b/runtime/hydradx/src/weights/pallet_btc_vault.rs new file mode 100644 index 0000000000..d784e18892 --- /dev/null +++ b/runtime/hydradx/src/weights/pallet_btc_vault.rs @@ -0,0 +1,39 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +pub struct HydraWeight(PhantomData); +impl pallet_btc_vault::WeightInfo for HydraWeight { + fn pause() -> Weight { + Weight::from_parts(7_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn unpause() -> Weight { + Weight::from_parts(7_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + fn request_deposit() -> Weight { + Weight::from_parts(150_000_000, 65536) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + fn claim_deposit() -> Weight { + Weight::from_parts(60_000_000, 4096) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + fn withdraw_btc() -> Weight { + Weight::from_parts(150_000_000, 65536) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + fn complete_withdraw_btc() -> Weight { + Weight::from_parts(60_000_000, 4096) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} diff --git a/scripts/dispenser-tests/btc-vault.test.ts b/scripts/dispenser-tests/btc-vault.test.ts new file mode 100644 index 0000000000..0a1ddbce5d --- /dev/null +++ b/scripts/dispenser-tests/btc-vault.test.ts @@ -0,0 +1,1001 @@ +import { ApiPromise, WsProvider, Keyring } from '@polkadot/api' +import { ISubmittableResult } from '@polkadot/types/types' +import { waitReady } from '@polkadot/wasm-crypto' +import { u8aToHex } from '@polkadot/util' +import { encodeAddress } from '@polkadot/keyring' +import { ethers } from 'ethers' +import * as bitcoin from 'bitcoinjs-lib' +import Client from 'bitcoin-core' +import { SignetClient } from './signet-client' +import { KeyDerivation } from './key-derivation' +import * as ecc from 'tiny-secp256k1' +import coinSelect from 'coinselect' + +bitcoin.initEccLib(ecc) + +// Must match the server's MPC_ROOT_KEY from .env +const MPC_ROOT_KEY = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' +const ROOT_PUBLIC_KEY = (() => { + const pub = ecc.pointFromScalar( + Buffer.from(MPC_ROOT_KEY.slice(2), 'hex'), + false, + ) + if (!pub) throw new Error('Invalid MPC root key') + return '0x' + Buffer.from(pub).toString('hex') +})() +const BTC_CAIP2 = 'bip122:000000000933ea01ad0ee984209779ba' +const SUBSTRATE_CHAIN_ID = 'polkadot:2034' +const SECP256K1_N = BigInt( + '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', +) +const ERROR_PREFIX = Buffer.from([0xde, 0xad, 0xbe, 0xef]) + +// ============ Helper Functions ============ + +function getPalletAccountId(): Uint8Array { + const data = new Uint8Array(32) + data.set(new TextEncoder().encode('modl'), 0) + data.set(new TextEncoder().encode('py/btcvt'), 4) + return data +} + +function stripScalePrefix(data: Uint8Array): Uint8Array { + if (data.length === 0) return data + const mode = data[0] & 0b11 + if (mode === 0) return data.slice(1) + if (mode === 1) return data.slice(2) + if (mode === 2) return data.slice(4) + return data +} + +function compressPubkey(pubKey: string): Buffer { + const uncompressed = Buffer.from(pubKey.slice(4), 'hex') + const full = Buffer.concat([Buffer.from([0x04]), uncompressed]) + return Buffer.from(ecc.pointCompress(full, true)) +} + +function btcAddressFromPubKey( + pubKey: string, + network: bitcoin.Network, +): string { + return bitcoin.payments.p2wpkh({ pubkey: compressPubkey(pubKey), network }) + .address! +} + +function normalizeSignature(r: Buffer, s: Buffer): { r: Buffer; s: Buffer } { + const sBigInt = BigInt('0x' + s.toString('hex')) + if (sBigInt > SECP256K1_N / 2n) { + return { + r, + s: Buffer.from( + (SECP256K1_N - sBigInt).toString(16).padStart(64, '0'), + 'hex', + ), + } + } + return { r, s } +} + +function extractSigBuffers(sig: any): { r: Buffer; s: Buffer } { + const r = + typeof sig.bigR.x === 'string' + ? Buffer.from(sig.bigR.x.slice(2), 'hex') + : Buffer.from(sig.bigR.x) + const s = + typeof sig.s === 'string' + ? Buffer.from(sig.s.slice(2), 'hex') + : Buffer.from(sig.s) + return normalizeSignature(r, s) +} + +function encodeDER(r: Buffer, s: Buffer): Buffer { + const toDER = (x: Buffer): Buffer => { + let i = 0 + while (i < x.length - 1 && x[i] === 0 && x[i + 1] < 0x80) i++ + const trimmed = x.subarray(i) + return trimmed[0] >= 0x80 + ? Buffer.concat([Buffer.from([0x00]), trimmed]) + : trimmed + } + const rDER = toDER(r), + sDER = toDER(s) + const len = 2 + rDER.length + 2 + sDER.length + const buf = Buffer.allocUnsafe(2 + len) + buf[0] = 0x30 + buf[1] = len + buf[2] = 0x02 + buf[3] = rDER.length + rDER.copy(buf, 4) + buf[4 + rDER.length] = 0x02 + buf[5 + rDER.length] = sDER.length + sDER.copy(buf, 6 + rDER.length) + return buf +} + +// Compute VAULT_PUBKEY_HASH from MPC root key derivation (must match runtime VaultPubkeyHash) +const VAULT_PUBKEY_HASH = (() => { + const palletSS58 = encodeAddress(getPalletAccountId(), 0) + const vaultPubKey = KeyDerivation.derivePublicKey( + ROOT_PUBLIC_KEY, + SUBSTRATE_CHAIN_ID, + palletSS58, + 'root', + ) + const compressed = compressPubkey(vaultPubKey) + return Array.from(bitcoin.crypto.hash160(compressed)) +})() + +function getVaultScript(): Buffer { + return Buffer.concat([ + Buffer.from([0x00, 0x14]), + Buffer.from(VAULT_PUBKEY_HASH), + ]) +} + +function getScriptCode( + witnessScript: Buffer, + network: bitcoin.Network, +): Buffer { + return Buffer.from( + bitcoin.payments.p2pkh({ + hash: Buffer.from(witnessScript.subarray(2)), + network, + }).output!, + ) +} + +async function submitWithRetry( + tx: any, + signer: any, + api: ApiPromise, + label: string, + maxRetries = 3, + timeoutMs = 60000, +): Promise<{ events: any[] }> { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await new Promise((resolve, reject) => { + let unsub: any + const timer = setTimeout(() => { + unsub?.() + reject(new Error('TIMEOUT')) + }, timeoutMs) + + tx.signAndSend(signer, (result: ISubmittableResult) => { + const { status, events, dispatchError } = result + if (status.isInBlock) { + clearTimeout(timer) + unsub?.() + if (dispatchError) { + const err = dispatchError.isModule + ? api.registry.findMetaError(dispatchError.asModule) + : { section: '', name: dispatchError.toString(), docs: [] } + reject(new Error(`${err.section}.${err.name}`)) + return + } + console.log(`✅ ${label} in block`) + resolve({ events: Array.from(events) }) + } else if (status.isInvalid || status.isDropped) { + clearTimeout(timer) + unsub?.() + reject(new Error(status.isInvalid ? 'INVALID_TX' : 'DROPPED')) + } + }) + .then((u: any) => (unsub = u)) + .catch((e: any) => { + clearTimeout(timer) + reject(e) + }) + }) + } catch (error: any) { + const msg = error.message || '' + if ( + (msg === 'INVALID_TX' || msg === 'TIMEOUT' || msg.includes('stale')) && + attempt < maxRetries + ) { + console.log(`🔄 Retrying ${label}...`) + await new Promise((r) => setTimeout(r, 2000)) + continue + } + throw error + } + } + throw new Error(`${label} failed after retries`) +} + +async function waitForSignature( + api: ApiPromise, + requestId: string, + timeout: number, +): Promise { + return new Promise((resolve, reject) => { + let unsub: any + const timer = setTimeout(() => { + unsub?.() + reject(new Error(`Timeout for ${requestId}`)) + }, timeout) + api.query.system + .events((events: any) => { + for (const { event } of events) { + if ( + event.section === 'signet' && + event.method === 'SignatureResponded' + ) { + const [reqId, , signature] = event.data + if (ethers.hexlify(reqId.toU8a()) === requestId) { + clearTimeout(timer) + unsub?.() + resolve(signature.toJSON()) + return + } + } + } + }) + .then((u: any) => (unsub = u)) + }) +} + +async function waitForResponse( + api: ApiPromise, + requestId: string, + timeout: number, +): Promise { + return new Promise((resolve) => { + let unsub: any + const timer = setTimeout(() => { + unsub?.() + resolve(null) + }, timeout) + api.query.system + .events((events: any) => { + for (const { event } of events) { + if ( + event.section === 'signet' && + event.method === 'RespondBidirectionalEvent' + ) { + const [reqId, , output, signature] = event.data + if (ethers.hexlify(reqId.toU8a()) === requestId) { + clearTimeout(timer) + unsub?.() + resolve({ + output: Array.from(output.toU8a()), + signature: signature.toJSON(), + }) + return + } + } + } + }) + .then((u: any) => (unsub = u)) + }) +} + +// ============ Test Suite ============ + +describe('BTC Vault Integration', () => { + let api: ApiPromise + let alice: any + let signetClient: SignetClient + let btcClient: any + let derivedBtcAddress: string + let derivedPubKey: string + let network: bitcoin.Network + let palletSS58: string + let spentVaultUtxos: Array<{ + txid: string + vout: number + value: number + script: Buffer + }> = [] + + beforeAll(async () => { + await waitReady() + + btcClient = new Client({ + host: 'http://localhost:18443', + username: 'test', + password: 'test123', + }) + await btcClient.command('getblockcount') // Verify connection + + api = await ApiPromise.create({ + provider: new WsProvider('ws://127.0.0.1:8000'), + types: { + AffinePoint: { x: '[u8; 32]', y: '[u8; 32]' }, + Signature: { big_r: 'AffinePoint', s: '[u8; 32]', recovery_id: 'u8' }, + }, + }) + + const keyring = new Keyring({ type: 'sr25519' }) + alice = keyring.addFromUri('//Alice') + palletSS58 = encodeAddress(getPalletAccountId(), 0) + + // Fund pallet if needed + const { data: palletBalance } = (await api.query.system.account( + palletSS58, + )) as any + if (palletBalance.free.toBigInt() < 10000000000000n) { + await submitWithRetry( + api.tx.balances.transferKeepAlive(palletSS58, 10000000000000n), + alice, + api, + 'Fund pallet', + ) + } + + signetClient = new SignetClient(api, alice) + await signetClient.ensureInitialized(SUBSTRATE_CHAIN_ID) + const aliceAccountId = keyring.decodeAddress(alice.address) + const aliceHexPath = '0x' + u8aToHex(aliceAccountId).slice(2) + + derivedPubKey = KeyDerivation.derivePublicKey( + ROOT_PUBLIC_KEY, + SUBSTRATE_CHAIN_ID, + palletSS58, + aliceHexPath, + ) + network = bitcoin.networks.regtest + derivedBtcAddress = btcAddressFromPubKey(derivedPubKey, network) + + // Fund Bitcoin address + let walletAddr + try { + walletAddr = await btcClient.command('getnewaddress') + } catch { + try { + await btcClient.command('createwallet', 'testwallet') + } catch { + await btcClient.command('loadwallet', 'testwallet') + } + walletAddr = await btcClient.command('getnewaddress') + } + await btcClient.command('generatetoaddress', 101, walletAddr) + await btcClient.command('sendtoaddress', derivedBtcAddress, 1.0) + await btcClient.command('generatetoaddress', 1, walletAddr) + console.log(`✅ Setup complete. Address: ${derivedBtcAddress}`) + }, 180000) + + afterAll(async () => { + await api?.disconnect() + }) + + it('should complete deposit and claim flow', async () => { + // Initialize vault if needed + const mpcEthAddress = new Uint8Array([ + 0x1b, 0xe3, 0x1a, 0x94, 0x36, 0x1a, 0x39, 0x1b, 0xba, 0xfb, 0x2a, 0x4c, + 0xcd, 0x70, 0x4f, 0x57, 0xdc, 0x04, 0xd4, 0xbb, + ]) + const existingConfig = await api.query.btcVault.palletConfig() + // if (existingConfig.toJSON() === null) { + // await submitWithRetry( + // api.tx.btcVault.initialize(Array.from(mpcEthAddress)), + // alice, + // api, + // 'Initialize vault', + // ) + // } + + // Get UTXOs and build transaction + const scanResult = await btcClient.command('scantxoutset', 'start', [ + `addr(${derivedBtcAddress})`, + ]) + expect(scanResult.unspents.length).toBeGreaterThan(0) + + const depositAmount = 35289790 + const utxos = scanResult.unspents.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: Math.floor(u.amount * 100000000), + script: Buffer.from( + bitcoin.address.toOutputScript(derivedBtcAddress, network), + ), + })) + + const coinSelectResult = coinSelect( + utxos, + [{ script: getVaultScript(), value: depositAmount }], + 2, + ) + if (!coinSelectResult.inputs || !coinSelectResult.outputs) + throw new Error('Insufficient funds') + const { inputs, outputs } = coinSelectResult + + // Build PSBT + const psbt = new bitcoin.Psbt({ network }) + for (const input of inputs) { + psbt.addInput({ + hash: Buffer.from(input.txid, 'hex').reverse(), + index: input.vout, + sequence: 0xffffffff, + witnessUtxo: { script: input.script!, value: BigInt(input.value) }, + }) + psbt.updateInput(psbt.inputCount - 1, { + sighashType: bitcoin.Transaction.SIGHASH_ALL, + }) + } + for (const output of outputs) { + psbt.addOutput( + output.script + ? { script: output.script, value: BigInt(output.value) } + : { address: derivedBtcAddress, value: BigInt(output.value) }, + ) + } + + const unsignedTx = bitcoin.Transaction.fromBuffer( + psbt.data.globalMap.unsignedTx.toBuffer(), + ) + const txid = Buffer.from(unsignedTx.getId(), 'hex') + const keyring = new Keyring({ type: 'sr25519' }) + const aliceHexPath = + '0x' + u8aToHex(keyring.decodeAddress(alice.address)).slice(2) + + // Calculate request IDs + const aggregateRequestId = signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from(txid), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: aliceHexPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ) + + const perInputRequestIds = inputs.map((_: any, i: number) => { + const indexBuf = Buffer.alloc(4) + indexBuf.writeUInt32LE(i, 0) + return ethers.hexlify( + signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from( + Buffer.concat([Buffer.from(unsignedTx.getId(), 'hex'), indexBuf]), + ), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: aliceHexPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ), + ) + }) + + // Submit deposit + const palletInputs = inputs.map((i: any) => ({ + txid: Array.from(Buffer.from(i.txid, 'hex')), + vout: i.vout, + value: i.value, + scriptPubkey: Array.from(i.script), + sequence: 0xffffffff, + })) + const palletOutputs = outputs.map((o: any) => ({ + value: o.value, + scriptPubkey: Array.from( + o.script || bitcoin.address.toOutputScript(derivedBtcAddress, network), + ), + })) + + const depositResult = await submitWithRetry( + api.tx.btcVault.requestDeposit( + Array.from(ethers.getBytes(aggregateRequestId)), + palletInputs, + palletOutputs, + 0, + ), + alice, + api, + 'Deposit BTC', + ) + + // Extract the pallet's PSBT from the SignBidirectionalRequested event + // The server signs sighashes from THIS PSBT, so we must verify against it + const bidirectionalEvent = depositResult.events.find( + (r: any) => + r.event.section === 'signet' && + r.event.method === 'SignBidirectionalRequested', + ) + expect(bidirectionalEvent).toBeTruthy() + + const palletPsbt = bitcoin.Psbt.fromBuffer( + Buffer.from(stripScalePrefix(bidirectionalEvent!.event.data[1].toU8a())), + ) + const palletUnsignedTx = bitcoin.Transaction.fromBuffer( + palletPsbt.data.globalMap.unsignedTx.toBuffer(), + ) + + // Verify pallet built the same transaction + console.log(` Local txid: ${unsignedTx.getId()}`) + console.log(` Pallet txid: ${palletUnsignedTx.getId()}`) + + // Wait for signatures and apply them using the pallet's PSBT for sighash + const compressedPubkey = compressPubkey(derivedPubKey) + for (let i = 0; i < inputs.length; i++) { + const sig = await waitForSignature(api, perInputRequestIds[i], 120000) + console.log('sig -> ', sig) + const { r, s } = extractSigBuffers(sig) + const witnessUtxo = palletPsbt.data.inputs[i].witnessUtxo! + const scriptCode = getScriptCode(Buffer.from(witnessUtxo.script), network) + const sighash = palletUnsignedTx.hashForWitnessV0( + i, + scriptCode, + witnessUtxo.value, + bitcoin.Transaction.SIGHASH_ALL, + ) + const verified = ecc.verify( + sighash, + compressedPubkey, + Buffer.concat([r, s]), + ) + console.log( + ` Input ${i}: sighash=${Buffer.from(sighash).toString('hex').slice(0, 16)}... ` + + `pubkey=${compressedPubkey.toString('hex').slice(0, 16)}... verified=${verified}`, + ) + expect(verified).toBe(true) + psbt.updateInput(i, { + partialSig: [ + { + pubkey: compressedPubkey, + signature: Buffer.concat([encodeDER(r, s), Buffer.from([0x01])]), + }, + ], + }) + } + + // Broadcast + psbt.finalizeAllInputs() + const signedTxHex = psbt.extractTransaction().toHex() + await btcClient.command('sendrawtransaction', signedTxHex) + await btcClient.command('generatetoaddress', 1, derivedBtcAddress) + + // Wait for read response and claim + const readResponse = await waitForResponse( + api, + ethers.hexlify(aggregateRequestId), + 120000, + ) + expect(readResponse).toBeTruthy() + + const outputBytes = stripScalePrefix(new Uint8Array(readResponse.output)) + + // Debug: verify signature off-chain before submitting + { + const requestIdBytes = ethers.getBytes(aggregateRequestId) + const msgData = Buffer.concat([Buffer.from(requestIdBytes), Buffer.from(outputBytes)]) + const msgHash = ethers.keccak256(msgData) + const sig = readResponse.signature + const r = typeof sig.bigR.x === 'string' ? sig.bigR.x : '0x' + Buffer.from(sig.bigR.x).toString('hex') + const s = typeof sig.s === 'string' ? sig.s : '0x' + Buffer.from(sig.s).toString('hex') + console.log(` DEBUG outputBytes (${outputBytes.length} bytes): ${Buffer.from(outputBytes).toString('hex')}`) + console.log(` DEBUG requestId: ${aggregateRequestId}`) + console.log(` DEBUG msgHash: ${msgHash}`) + console.log(` DEBUG sig r: ${r}`) + console.log(` DEBUG sig s: ${s}`) + console.log(` DEBUG sig recoveryId: ${sig.recoveryId}`) + // Try all recovery IDs + for (const rid of [0, 1]) { + try { + const v = BigInt(rid + 27) + const recovered = ethers.recoverAddress(msgHash, { r, s, v }) + const expected = '0x' + Buffer.from([ + 0x1b, 0xe3, 0x1a, 0x94, 0x36, 0x1a, 0x39, 0x1b, 0xba, 0xfb, + 0x2a, 0x4c, 0xcd, 0x70, 0x4f, 0x57, 0xdc, 0x04, 0xd4, 0xbb, + ]).toString('hex') + console.log(` DEBUG recoveryId=${rid}: recovered=${recovered} expected=${expected} match=${recovered.toLowerCase() === expected.toLowerCase()}`) + } catch (e: any) { + console.log(` DEBUG recoveryId=${rid}: FAILED - ${e.message}`) + } + } + } + + const balanceBefore = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + + await submitWithRetry( + api.tx.btcVault.claimDeposit( + Array.from(ethers.getBytes(aggregateRequestId)), + Array.from(outputBytes), + readResponse.signature, + ), + alice, + api, + 'Claim BTC', + ) + + const balanceAfter = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect((balanceAfter - balanceBefore).toString()).toBe( + depositAmount.toString(), + ) + console.log(`✅ Deposit claimed: ${balanceAfter - balanceBefore} sats`) + }, 300000) + + it('should complete successful withdrawal', async () => { + const balanceBefore = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect(balanceBefore).toBeGreaterThan(0n) + + const vaultScript = getVaultScript() + const vaultAddress = bitcoin.address.fromOutputScript(vaultScript, network) + const vaultScan = await btcClient.command('scantxoutset', 'start', [ + `addr(${vaultAddress})`, + ]) + if (vaultScan.unspents.length === 0) return + + const withdrawAmount = 10000000 + const vaultUtxos = vaultScan.unspents.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: Math.floor(u.amount * 100000000), + script: vaultScript, + })) + const recipientScript = Buffer.from( + bitcoin.address.toOutputScript(derivedBtcAddress, network), + ) + + const coinSelectResult = coinSelect( + vaultUtxos, + [{ script: recipientScript, value: withdrawAmount }], + 2, + ) + if (!coinSelectResult.inputs || !coinSelectResult.outputs) return + const { inputs, outputs } = coinSelectResult + + spentVaultUtxos = inputs.map((i: any) => ({ + txid: i.txid, + vout: i.vout, + value: i.value, + script: i.script, + })) + + // Build PSBT + const psbt = new bitcoin.Psbt({ network }) + for (const input of inputs) { + psbt.addInput({ + hash: Buffer.from(input.txid, 'hex').reverse(), + index: input.vout, + sequence: 0xffffffff, + witnessUtxo: { script: input.script!, value: BigInt(input.value) }, + }) + psbt.updateInput(psbt.inputCount - 1, { + sighashType: bitcoin.Transaction.SIGHASH_ALL, + }) + } + for (const output of outputs) { + psbt.addOutput( + output.script + ? { script: output.script, value: BigInt(output.value) } + : { script: vaultScript, value: BigInt(output.value) }, + ) + } + + const unsignedTx = bitcoin.Transaction.fromBuffer( + psbt.data.globalMap.unsignedTx.toBuffer(), + ) + const txid = Buffer.from(unsignedTx.getId(), 'hex') + const withdrawalPath = 'root' + + const aggregateRequestId = signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from(txid), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: withdrawalPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ) + + const perInputRequestIds = inputs.map((_: any, i: number) => { + const indexBuf = Buffer.alloc(4) + indexBuf.writeUInt32LE(i, 0) + return ethers.hexlify( + signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from( + Buffer.concat([Buffer.from(unsignedTx.getId(), 'hex'), indexBuf]), + ), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: withdrawalPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ), + ) + }) + + const palletInputs = inputs.map((i: any) => ({ + txid: Array.from(Buffer.from(i.txid, 'hex')), + vout: i.vout, + value: i.value, + scriptPubkey: Array.from(i.script), + sequence: 0xffffffff, + })) + const palletOutputs = outputs.map((o: any) => ({ + value: o.value, + scriptPubkey: Array.from(o.script || vaultScript), + })) + + const withdrawResult = await submitWithRetry( + api.tx.btcVault.withdrawBtc( + Array.from(ethers.getBytes(aggregateRequestId)), + withdrawAmount, + Array.from(recipientScript), + palletInputs, + palletOutputs, + 0, + ), + alice, + api, + 'Withdraw BTC', + ) + expect( + withdrawResult.events.some( + (r: any) => + r.event.section === 'btcVault' && + r.event.method === 'WithdrawalRequested', + ), + ).toBe(true) + + // Verify optimistic decrement + const balanceAfterWithdraw = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect(balanceAfterWithdraw).toBe(balanceBefore - BigInt(withdrawAmount)) + + // Get signatures + const vaultPubKey = KeyDerivation.derivePublicKey( + ROOT_PUBLIC_KEY, + SUBSTRATE_CHAIN_ID, + palletSS58, + withdrawalPath, + ) + const vaultCompressed = compressPubkey(vaultPubKey) + + for (let i = 0; i < inputs.length; i++) { + const sig = await waitForSignature(api, perInputRequestIds[i], 120000) + const { r, s } = extractSigBuffers(sig) + psbt.updateInput(i, { + partialSig: [ + { + pubkey: vaultCompressed, + signature: Buffer.concat([encodeDER(r, s), Buffer.from([0x01])]), + }, + ], + }) + } + + // Broadcast + psbt.finalizeAllInputs() + await btcClient.command( + 'sendrawtransaction', + psbt.extractTransaction().toHex(), + ) + await btcClient.command('generatetoaddress', 1, derivedBtcAddress) + + // Complete withdrawal + const readResponse = await waitForResponse( + api, + ethers.hexlify(aggregateRequestId), + 120000, + ) + expect(readResponse).toBeTruthy() + + const outputBytes = stripScalePrefix(new Uint8Array(readResponse.output)) + const completeResult = await submitWithRetry( + api.tx.btcVault.completeWithdrawBtc( + Array.from(ethers.getBytes(aggregateRequestId)), + Array.from(outputBytes), + readResponse.signature, + ), + alice, + api, + 'Complete Withdraw', + ) + + expect( + completeResult.events.some( + (r: any) => + r.event.section === 'btcVault' && + r.event.method === 'WithdrawalCompleted', + ), + ).toBe(true) + console.log(`✅ Withdrawal completed: ${withdrawAmount} sats`) + }, 300000) + + it('should refund when withdrawal fails', async () => { + if (spentVaultUtxos.length === 0) return + + const balanceBefore = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect(balanceBefore).toBeGreaterThan(0n) + + const vaultScript = getVaultScript() + const recipientScript = Buffer.from( + bitcoin.address.toOutputScript(derivedBtcAddress, network), + ) + const totalValue = spentVaultUtxos.reduce((sum, u) => sum + u.value, 0) + const withdrawAmount = Math.min(5000000, totalValue - 1000) + const fee = 500 + + const inputs = spentVaultUtxos + const outputs = [ + { script: recipientScript, value: withdrawAmount }, + { script: vaultScript, value: totalValue - withdrawAmount - fee }, + ] + + // Build PSBT with spent UTXOs + const psbt = new bitcoin.Psbt({ network }) + for (const input of inputs) { + psbt.addInput({ + hash: Buffer.from(input.txid, 'hex').reverse(), + index: input.vout, + sequence: 0xffffffff, + witnessUtxo: { script: input.script!, value: BigInt(input.value) }, + }) + psbt.updateInput(psbt.inputCount - 1, { + sighashType: bitcoin.Transaction.SIGHASH_ALL, + }) + } + for (const output of outputs) { + psbt.addOutput({ script: output.script, value: BigInt(output.value) }) + } + + const unsignedTx = bitcoin.Transaction.fromBuffer( + psbt.data.globalMap.unsignedTx.toBuffer(), + ) + const txid = Buffer.from(unsignedTx.getId(), 'hex') + const withdrawalPath = 'root' + + const aggregateRequestId = signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from(txid), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: withdrawalPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ) + + const perInputRequestIds = inputs.map((_, i) => { + const indexBuf = Buffer.alloc(4) + indexBuf.writeUInt32LE(i, 0) + return ethers.hexlify( + signetClient.calculateSignRespondRequestId( + palletSS58, + Array.from( + Buffer.concat([Buffer.from(unsignedTx.getId(), 'hex'), indexBuf]), + ), + { + caip2Id: BTC_CAIP2, + keyVersion: 0, + path: withdrawalPath, + algo: 'ecdsa', + dest: 'bitcoin', + params: '', + }, + ), + ) + }) + + const palletInputs = inputs.map((i: any) => ({ + txid: Array.from(Buffer.from(i.txid, 'hex')), + vout: i.vout, + value: i.value, + scriptPubkey: Array.from(i.script), + sequence: 0xffffffff, + })) + const palletOutputs = outputs.map((o: any) => ({ + value: o.value, + scriptPubkey: Array.from(o.script), + })) + + await submitWithRetry( + api.tx.btcVault.withdrawBtc( + Array.from(ethers.getBytes(aggregateRequestId)), + withdrawAmount, + Array.from(recipientScript), + palletInputs, + palletOutputs, + 0, + ), + alice, + api, + 'Withdraw BTC (refund)', + ) + + const balanceAfterWithdraw = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect(balanceAfterWithdraw).toBe(balanceBefore - BigInt(withdrawAmount)) + + // Get signatures + const vaultPubKey = KeyDerivation.derivePublicKey( + ROOT_PUBLIC_KEY, + SUBSTRATE_CHAIN_ID, + palletSS58, + withdrawalPath, + ) + const vaultCompressed = compressPubkey(vaultPubKey) + + for (let i = 0; i < inputs.length; i++) { + const sig = await waitForSignature(api, perInputRequestIds[i], 120000) + const { r, s } = extractSigBuffers(sig) + psbt.updateInput(i, { + partialSig: [ + { + pubkey: vaultCompressed, + signature: Buffer.concat([encodeDER(r, s), Buffer.from([0x01])]), + }, + ], + }) + } + + psbt.finalizeAllInputs() + const signedTxHex = psbt.extractTransaction().toHex() + + // Broadcast should fail (spent UTXOs) + let broadcastFailed = false + try { + await btcClient.command('sendrawtransaction', signedTxHex) + } catch { + broadcastFailed = true + } + expect(broadcastFailed).toBe(true) + console.log('✅ Broadcast failed as expected (spent UTXOs)') + + // Wait for MPC error response + const readResponse = await waitForResponse( + api, + ethers.hexlify(aggregateRequestId), + 300000, + ) + expect(readResponse).toBeTruthy() + + const outputBytes = stripScalePrefix(new Uint8Array(readResponse.output)) + expect(Buffer.from(outputBytes.slice(0, 4)).equals(ERROR_PREFIX)).toBe(true) + + // Complete withdrawal with error + const completeResult = await submitWithRetry( + api.tx.btcVault.completeWithdrawBtc( + Array.from(ethers.getBytes(aggregateRequestId)), + Array.from(outputBytes), + readResponse.signature, + ), + alice, + api, + 'Complete Withdraw (refund)', + ) + + expect( + completeResult.events.some( + (r: any) => + r.event.section === 'btcVault' && + r.event.method === 'WithdrawalFailed', + ), + ).toBe(true) + + // Verify refund + const finalBalance = BigInt( + (await api.query.btcVault.userBalances(alice.address)).toString(), + ) + expect(finalBalance).toBe(balanceBefore) + console.log(`✅ Refund verified: balance restored to ${finalBalance} sats`) + }, 360000) +}) diff --git a/scripts/dispenser-tests/coinselect.d.ts b/scripts/dispenser-tests/coinselect.d.ts new file mode 100644 index 0000000000..64e9713de5 --- /dev/null +++ b/scripts/dispenser-tests/coinselect.d.ts @@ -0,0 +1,26 @@ +declare module 'coinselect' { + interface UTXO { + txid: string + vout: number + value: number + script: Buffer + [key: string]: any + } + + interface Target { + script: Buffer + value: number + } + + interface CoinSelectResult { + inputs?: UTXO[] + outputs?: (Target & { [key: string]: any })[] + fee: number + } + + export default function coinSelect( + utxos: UTXO[], + targets: Target[], + feeRate: number, + ): CoinSelectResult +} diff --git a/scripts/dispenser-tests/key-derivation.ts b/scripts/dispenser-tests/key-derivation.ts index 538b4289e0..1529da743f 100644 --- a/scripts/dispenser-tests/key-derivation.ts +++ b/scripts/dispenser-tests/key-derivation.ts @@ -6,13 +6,13 @@ export class KeyDerivation { static derivePublicKey( rootPublicKey: string, - chainId: string + ...pathComponents: string[] ): string { const ec = new EC("secp256k1"); const uncompressedRoot = rootPublicKey.slice(4); - const derivationPath = `${this.EPSILON_PREFIX},${chainId}`; + const derivationPath = `${this.EPSILON_PREFIX},${pathComponents.join(',')}`; const hash = ethers.keccak256(ethers.toUtf8Bytes(derivationPath)); const scalarHex = hash.slice(2); diff --git a/scripts/dispenser-tests/package-lock.json b/scripts/dispenser-tests/package-lock.json index 04a8f8db87..daebae441b 100644 --- a/scripts/dispenser-tests/package-lock.json +++ b/scripts/dispenser-tests/package-lock.json @@ -14,9 +14,13 @@ "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^13.5.9", "@polkadot/wasm-crypto": "^7.5.1", + "bitcoin-core": "^5.0.0", + "bitcoinjs-lib": "^7.0.1", + "coinselect": "^3.1.13", "dotenv": "^17.2.3", "elliptic": "^6.6.1", "ethers": "^6.15.0", + "tiny-secp256k1": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.3.3", "viem": "^2.37.6" @@ -584,6 +588,40 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1254,6 +1292,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -3069,6 +3120,17 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3234,6 +3296,34 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", @@ -3248,6 +3338,248 @@ "darwin" ] }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@uphold/request-logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@uphold/request-logger/-/request-logger-2.0.0.tgz", + "integrity": "sha512-UvGS+v87C7VTtQDcFHDLfvfl1zaZaLSwSmAnV35Ne7CzAVvotmZqt9lAIoNpMpaoRpdjVIcnUDwPSeIeA//EoQ==", + "license": "MIT", + "dependencies": { + "uuid": "^3.0.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "request": ">=2.27.0" + } + }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", @@ -3299,6 +3631,22 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3384,6 +3732,45 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -3487,7 +3874,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -3500,17 +3893,99 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bip174": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz", + "integrity": "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bitcoin-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bitcoin-core/-/bitcoin-core-5.0.0.tgz", + "integrity": "sha512-XqHsD5LjtshN8yWzRrq2kof57e1eXCGDx3i5+sFKBRi9MktSlXOR4SRLyXLkfzfBmPEs5q/76RotQJuaWg75DQ==", + "license": "MIT", + "dependencies": { + "@uphold/request-logger": "^2.0.0", + "debugnyan": "^1.0.0", + "json-bigint": "^1.0.0", + "lodash": "^4.0.0", + "request": "^2.53.0", + "semver": "^5.1.0", + "standard-error": "^1.1.0" + }, + "engines": { + "node": ">=7" + } + }, + "node_modules/bitcoin-core/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.1.tgz", + "integrity": "sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^1.2.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3583,6 +4058,25 @@ "node": ">= 6" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3600,6 +4094,24 @@ "dev": true, "license": "MIT" }, + "node_modules/bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "engines": [ + "node >=0.10.0" + ], + "license": "MIT", + "bin": { + "bunyan": "bin/bunyan" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3644,6 +4156,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3738,6 +4256,12 @@ "node": ">= 0.12.0" } }, + "node_modules/coinselect": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/coinselect/-/coinselect-3.1.13.tgz", + "integrity": "sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg==", + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", @@ -3765,11 +4289,23 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -3779,6 +4315,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3800,6 +4342,18 @@ "node": ">= 8" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3826,6 +4380,34 @@ } } }, + "node_modules/debugnyan": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/debugnyan/-/debugnyan-1.0.0.tgz", + "integrity": "sha512-dTvKxcLZCammDLFYi31NRVr5g6vjJ33uf1wcdbIPPxPxxnJ9/xj00Mh/YQkhFMw/VGavaG5KpjSC+4o9r/JjRg==", + "license": "MIT", + "dependencies": { + "bunyan": "^1.8.1", + "debug": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debugnyan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/debugnyan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/dedent": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", @@ -3851,6 +4433,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3882,6 +4473,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3889,6 +4494,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -4140,11 +4755,31 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fb-watchman": { @@ -4224,6 +4859,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -4300,6 +4958,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -4376,6 +5043,29 @@ "uglify-js": "^3.1.4" } }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4414,6 +5104,21 @@ "dev": true, "license": "MIT" }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4458,7 +5163,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -4521,6 +5226,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4543,6 +5254,12 @@ "ws": "*" } }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5384,6 +6101,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5397,6 +6120,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -5404,6 +6136,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -5423,6 +6167,21 @@ "node": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5453,6 +6212,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5520,6 +6285,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5546,7 +6332,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5559,7 +6345,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5575,6 +6361,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mock-socket": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", @@ -5584,12 +6383,44 @@ "node": ">= 8" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT", + "optional": true + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5613,6 +6444,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "license": "MIT", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -5708,11 +6549,20 @@ "node": ">=8" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5886,7 +6736,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5919,6 +6769,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5999,6 +6855,27 @@ "node": ">= 8" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -6016,6 +6893,15 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6023,6 +6909,38 @@ "dev": true, "license": "MIT" }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6056,6 +6974,38 @@ "node": ">=8" } }, + "node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -6065,6 +7015,39 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scale-ts": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz", @@ -6191,6 +7174,31 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6204,6 +7212,11 @@ "node": ">=10" } }, + "node_modules/standard-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/standard-error/-/standard-error-1.1.0.tgz", + "integrity": "sha512-4v7qzU7oLJfMI5EltUSHCaaOd65J6S4BqKRWgzMi4EYaE5fvNabPxmAPGdxpGXqrcWjhDGI/H09CIdEuUOUeXg==" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6374,6 +7387,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6394,6 +7428,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -6496,6 +7543,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6546,6 +7611,15 @@ "node": ">=0.8.0" } }, + "node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6619,6 +7693,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6640,6 +7733,52 @@ "node": ">=10.12.0" } }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/viem": { "version": "2.44.4", "resolved": "https://registry.npmjs.org/viem/-/viem-2.44.4.tgz", @@ -6835,7 +7974,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/scripts/dispenser-tests/package.json b/scripts/dispenser-tests/package.json index 4149e1dd3e..430a845e17 100644 --- a/scripts/dispenser-tests/package.json +++ b/scripts/dispenser-tests/package.json @@ -22,9 +22,13 @@ "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^13.5.9", "@polkadot/wasm-crypto": "^7.5.1", + "bitcoin-core": "^5.0.0", + "bitcoinjs-lib": "^7.0.1", + "coinselect": "^3.1.13", "dotenv": "^17.2.3", "elliptic": "^6.6.1", "ethers": "^6.15.0", + "tiny-secp256k1": "^2.2.4", "ts-node": "^10.9.2", "typescript": "^5.3.3", "viem": "^2.37.6" diff --git a/scripts/dispenser-tests/signet-client.ts b/scripts/dispenser-tests/signet-client.ts index 747b545b72..60d770b4cf 100644 --- a/scripts/dispenser-tests/signet-client.ts +++ b/scripts/dispenser-tests/signet-client.ts @@ -163,7 +163,7 @@ export class SignetClient { [ sender, txHex, - params.caip2_id, + params.caip2Id, params.keyVersion, params.path, params.algo || '', @@ -224,4 +224,24 @@ export class SignetClient { return recoveredAddress.toLowerCase() === expectedAddress.toLowerCase() } + + async ensureInitialized(chainId: string): Promise { + const admin = await this.api.query.signet.admin() + if (!admin.isEmpty) { + console.log('Signet already initialized, skipping') + return + } + + const chainIdBytes = Array.from(new TextEncoder().encode(chainId)) + const signetInitCall = this.api.tx.signet.initialize( + this.signer.address, + 1_000_000_000_000n, + chainIdBytes, + ) + await executeAsRootViaScheduler( + this.api, + signetInitCall, + 'Initialize signet via Root', + ) + } } diff --git a/scripts/dispenser-tests/yarn.lock b/scripts/dispenser-tests/yarn.lock index f56a80d5d7..423ead8499 100644 --- a/scripts/dispenser-tests/yarn.lock +++ b/scripts/dispenser-tests/yarn.lock @@ -603,7 +603,7 @@ dependencies: "@noble/hashes" "1.8.0" -"@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3", "@noble/hashes@^1.8.0", "@noble/hashes@~1.8.0", "@noble/hashes@1.8.0": +"@noble/hashes@^1.2.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3", "@noble/hashes@^1.8.0", "@noble/hashes@~1.8.0", "@noble/hashes@1.8.0": version "1.8.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== @@ -1396,6 +1396,13 @@ resolved "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz" integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== +"@uphold/request-logger@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@uphold/request-logger/-/request-logger-2.0.0.tgz" + integrity sha512-UvGS+v87C7VTtQDcFHDLfvfl1zaZaLSwSmAnV35Ne7CzAVvotmZqt9lAIoNpMpaoRpdjVIcnUDwPSeIeA//EoQ== + dependencies: + uuid "^3.0.1" + abitype@^1.2.3, abitype@1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz" @@ -1418,6 +1425,16 @@ aes-js@4.0.0-beta.5: resolved "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz" integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" @@ -1472,6 +1489,33 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@^1.0.0, assert-plus@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.13.2" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== + babel-jest@30.2.0: version "30.2.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz" @@ -1537,11 +1581,67 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== + baseline-browser-mapping@^2.9.0: version "2.9.15" resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz" integrity sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg== +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + +bignumber.js@^9.0.0: + version "9.3.1" + resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +bip174@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz" + integrity sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw== + dependencies: + uint8array-tools "^0.0.9" + varuint-bitcoin "^2.0.0" + +bitcoin-core@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/bitcoin-core/-/bitcoin-core-5.0.0.tgz" + integrity sha512-XqHsD5LjtshN8yWzRrq2kof57e1eXCGDx3i5+sFKBRi9MktSlXOR4SRLyXLkfzfBmPEs5q/76RotQJuaWg75DQ== + dependencies: + "@uphold/request-logger" "^2.0.0" + debugnyan "^1.0.0" + json-bigint "^1.0.0" + lodash "^4.0.0" + request "^2.53.0" + semver "^5.1.0" + standard-error "^1.1.0" + +bitcoinjs-lib@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.1.tgz" + integrity sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww== + dependencies: + "@noble/hashes" "^1.2.0" + bech32 "^2.0.0" + bip174 "^3.0.0" + bs58check "^4.0.0" + uint8array-tools "^0.0.9" + valibot "^1.2.0" + varuint-bitcoin "^2.0.0" + bn.js@^4.11.9: version "4.12.2" resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz" @@ -1597,6 +1697,21 @@ bs-logger@^0.2.6: dependencies: fast-json-stable-stringify "2.x" +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + +bs58check@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz" + integrity sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g== + dependencies: + "@noble/hashes" "^1.2.0" + bs58 "^6.0.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" @@ -1609,6 +1724,16 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +bunyan@^1.8.1: + version "1.8.15" + resolved "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + callsites@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -1629,6 +1754,11 @@ caniuse-lite@^1.0.30001759: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz" integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -1666,6 +1796,11 @@ co@^4.6.0: resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== +coinselect@^3.1.13: + version "3.1.13" + resolved "https://registry.npmjs.org/coinselect/-/coinselect-3.1.13.tgz" + integrity sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg== + collect-v8-coverage@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz" @@ -1683,6 +1818,13 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -1693,6 +1835,11 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + create-require@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" @@ -1707,11 +1854,25 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" @@ -1719,6 +1880,14 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "^2.1.3" +debugnyan@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/debugnyan/-/debugnyan-1.0.0.tgz" + integrity sha512-dTvKxcLZCammDLFYi31NRVr5g6vjJ33uf1wcdbIPPxPxxnJ9/xj00Mh/YQkhFMw/VGavaG5KpjSC+4o9r/JjRg== + dependencies: + bunyan "^1.8.1" + debug "^2.2.0" + dedent@^1.6.0: version "1.7.1" resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz" @@ -1729,6 +1898,11 @@ deepmerge@^4.3.1: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + detect-newline@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -1744,11 +1918,26 @@ dotenv@^17.2.3: resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz" integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + electron-to-chromium@^1.5.263: version "1.5.267" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz" @@ -1859,7 +2048,22 @@ expect@^30.0.0, expect@30.2.0: jest-mock "30.2.0" jest-util "30.2.0" -fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@^1.2.0, extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1902,6 +2106,20 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + formdata-polyfill@^4.0.10: version "4.0.10" resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" @@ -1939,6 +2157,13 @@ get-stream@^6.0.0: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + glob@^10.3.10: version "10.5.0" resolved "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz" @@ -1951,6 +2176,17 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.4: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -1980,6 +2216,19 @@ handlebars@^4.7.8: optionalDependencies: uglify-js "^3.1.4" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" @@ -2007,6 +2256,15 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -2063,6 +2321,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -2073,6 +2336,11 @@ isows@1.0.7: resolved "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz" integrity sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg== +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" @@ -2493,17 +2761,39 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== -json-stringify-safe@^5.0.1: +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== @@ -2513,6 +2803,16 @@ json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" @@ -2535,6 +2835,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== +lodash@^4.0.0: + version "4.17.23" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" @@ -2579,6 +2884,18 @@ micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -2594,7 +2911,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.4, minimatch@^3.1.1: +minimatch@^3.0.4, minimatch@^3.1.1, "minimatch@2 || 3": version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -2608,7 +2925,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.5: +minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -2618,16 +2935,47 @@ minimist@^1.2.5: resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mock-socket@^9.3.1: version "9.3.1" resolved "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz" integrity sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw== +moment@^2.19.3: + version "2.30.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +mv@~2: + version "2.1.1" + resolved "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + +nan@^2.14.0: + version "2.25.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz" + integrity sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g== + napi-postinstall@^0.3.0: version "0.3.4" resolved "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz" @@ -2638,6 +2986,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" @@ -2688,6 +3041,11 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + once@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -2780,6 +3138,11 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" @@ -2821,16 +3184,59 @@ propagate@^2.0.0: resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== +psl@^1.1.28: + version "1.15.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + pure-rand@^7.0.0: version "7.0.1" resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz" integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + react-is@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +request@^2.53.0: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" @@ -2848,6 +3254,13 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + rxjs@^7.8.1: version "7.8.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz" @@ -2855,11 +3268,31 @@ rxjs@^7.8.1: dependencies: tslib "^2.1.0" +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + scale-ts@^1.6.0: version "1.6.1" resolved "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz" integrity sha512-PBMc2AWc6wSEqJYBDPcyCLUj9/tMKnLX70jLOSndMtcUoLQucP/DM0vnQo1wJAYjTrQiq8iG9rD0q6wFzgjH7g== +semver@^5.1.0: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" @@ -2922,6 +3355,21 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sshpk@^1.7.0: + version "1.18.0" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" @@ -2929,6 +3377,11 @@ stack-utils@^2.0.6: dependencies: escape-string-regexp "^2.0.0" +standard-error@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/standard-error/-/standard-error-1.1.0.tgz" + integrity sha512-4v7qzU7oLJfMI5EltUSHCaaOd65J6S4BqKRWgzMi4EYaE5fvNabPxmAPGdxpGXqrcWjhDGI/H09CIdEuUOUeXg== + string-length@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -3039,6 +3492,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +tiny-secp256k1@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz" + integrity sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q== + dependencies: + uint8array-tools "0.0.7" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -3051,6 +3511,14 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + ts-jest@^29.4.4: version "29.4.6" resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz" @@ -3095,6 +3563,18 @@ tslib@2.7.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" @@ -3120,6 +3600,21 @@ uglify-js@^3.1.4: resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== +uint8array-tools@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz" + integrity sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g== + +uint8array-tools@^0.0.9: + version "0.0.9" + resolved "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz" + integrity sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A== + +uint8array-tools@0.0.7: + version "0.0.7" + resolved "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz" + integrity sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ== + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" @@ -3170,6 +3665,18 @@ update-browserslist-db@^1.2.0: escalade "^3.2.0" picocolors "^1.1.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +uuid@^3.0.1, uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" @@ -3184,6 +3691,27 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +valibot@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz" + integrity sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg== + +varuint-bitcoin@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz" + integrity sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog== + dependencies: + uint8array-tools "^0.0.8" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + viem@^2.37.6: version "2.44.4" resolved "https://registry.npmjs.org/viem/-/viem-2.44.4.tgz"