diff --git a/devenv/integration/bin/test b/devenv/integration/bin/test index 6a90ecb9..819b7214 100755 --- a/devenv/integration/bin/test +++ b/devenv/integration/bin/test @@ -5,12 +5,12 @@ ENTRYPOINT_PATH="./integration/docker/entrypoint" CONFIG_PATH="./sbtc/docker/config.json" project_name() { - local input="$1" + local input="$@" local filter="${input//[![:alnum:]]/_}" echo "$filter" } -filters=("test(deposit)" "test(withdrawal)") +filters=("test(deposit) or test(withdrawal)") filter_union="" ids=() projects=() diff --git a/romeo/examples/deposit_withdrawal.rs b/romeo/examples/deposit_withdrawal.rs new file mode 100644 index 00000000..80b13757 --- /dev/null +++ b/romeo/examples/deposit_withdrawal.rs @@ -0,0 +1,182 @@ +use std::{io::Cursor, time::Duration}; + +use bdk::{ + bitcoin::{psbt::serialize::Serialize, PrivateKey}, + blockchain::{ + ConfigurableBlockchain, ElectrumBlockchain, ElectrumBlockchainConfig, + }, + database::MemoryDatabase, + template::P2Wpkh, + SyncOptions, Wallet, +}; +use blockstack_lib::{ + codec::StacksMessageCodec, + util::hash::hex_bytes, + vm::{ + types::{QualifiedContractIdentifier, StandardPrincipalData}, + Value, + }, +}; +use romeo::{config::Config, stacks_client::StacksClient}; +use sbtc_cli::commands::{ + broadcast::{broadcast_tx, BroadcastArgs}, + deposit::{build_deposit_tx, DepositArgs}, + withdraw::{build_withdrawal_tx, WithdrawalArgs}, +}; +use stacks_core::address::StacksAddress; +use tokio::time::sleep; +use url::Url; + +/// Wait until all your services are ready before running. +/// Don't forget to fund W0 (deployer) and W1 (recipient). +#[tokio::main] +async fn main() { + let mut config = + Config::from_path("./devenv/sbtc/docker/config.json").unwrap(); + config.stacks_node_url = "http://localhost:3999".parse().unwrap(); + config.bitcoin_node_url = "http://localhost:18443".parse().unwrap(); + config.electrum_node_url = "tcp://localhost:60401".parse().unwrap(); + + let blockchain = + ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { + url: config.electrum_node_url.clone().into(), + socks5: None, + retry: 3, + timeout: Some(10), + stop_gap: 10, + validate_domain: false, + }) + .unwrap(); + + let recipient_p2wpkh_wif = + "cNcXK2r8bNdWJQymtAW8tGS7QHNtFFvG5CdXqhhT752u29WspXRM"; + + // W1 + let wallet = { + let private_key = PrivateKey::from_wif(recipient_p2wpkh_wif).unwrap(); + + Wallet::new( + P2Wpkh(private_key), + Some(P2Wpkh(private_key)), + bdk::bitcoin::Network::Regtest, + MemoryDatabase::default(), + ) + .unwrap() + }; + + loop { + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + let balance = wallet.get_balance().unwrap().confirmed; + println!("recipient's btc: {balance}"); + if balance != 0 { + break; + } + sleep(Duration::from_secs(1)).await; + } + + let recipient = "ST2ST2H80NP5C9SPR4ENJ1Z9CDM9PKAJVPYWPQZ50"; + let amount = 1000; + + // deposit + { + let electrum_url = + Url::parse(config.electrum_node_url.as_str()).unwrap(); + let tx = { + let args = DepositArgs { + node_url: electrum_url.clone(), + wif: recipient_p2wpkh_wif.into(), + network: bdk::bitcoin::Network::Regtest, + recipient:recipient.into(), + amount, + sbtc_wallet: "bcrt1pte5zmd7qzj4hdu45lh9mmdm0nwq3z35pwnxmzkwld6y0a8g83nnqhj6vc0".into(), + }; + + build_deposit_tx(&args).unwrap() + }; + + broadcast_tx(&BroadcastArgs { + node_url: electrum_url, + tx: hex::encode(tx.serialize()), + }) + .unwrap(); + } + + let deployer = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; + let deployer_address = StacksAddress::transmute_stacks_address(deployer); + let recipient_address = StacksAddress::transmute_stacks_address(recipient); + + let stacks_client = + StacksClient::new(config.clone(), reqwest::Client::new()); + + println!("Waiting on sBTC mint"); + // request token balance from the asset contract. + while { + let res: serde_json::Value = stacks_client + .call_read_only_fn( + QualifiedContractIdentifier::new( + StandardPrincipalData::from(deployer_address), + config.contract_name.clone(), + ), + "get-balance", + recipient_address.to_string().as_str(), + vec![StandardPrincipalData::from(recipient_address).into()], + ) + .await + .unwrap(); + + assert!(res["okay"].as_bool().unwrap()); + let bytes = + hex_bytes(res["result"].as_str().unwrap().trim_start_matches("0x")) + .unwrap(); + + let mut cursor = Cursor::new(&bytes); + Value::consensus_deserialize(&mut cursor) + .unwrap() + .expect_result_ok() + .expect_u128() + } < amount as u128 + { + sleep(Duration::from_secs(2)).await; + } + + let fee = 2000; + // withdraw + let args = WithdrawalArgs { + node_url: config.electrum_node_url.clone(), + network: bdk::bitcoin::Network::Regtest, + // p2wpkh + wif: "cNcXK2r8bNdWJQymtAW8tGS7QHNtFFvG5CdXqhhT752u29WspXRM".into(), + // Stacks + drawee_wif: "cR9hENRFiuHzKpj9B3QCTBrt19c5ZCJKHJwYcqj5dfB6aKyf6ndm" + .into(), + payee_address: "bcrt1q3zl64vadtuh3vnsuhdgv6pm93n82ye8q6cr4ch".into(), + amount, + fulfillment_fee: fee, + sbtc_wallet: + "bcrt1pte5zmd7qzj4hdu45lh9mmdm0nwq3z35pwnxmzkwld6y0a8g83nnqhj6vc0" + .into(), + }; + + let tx = build_withdrawal_tx(&args).unwrap(); + + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + let balance = wallet.get_balance().unwrap().confirmed; + + broadcast_tx(&BroadcastArgs { + node_url: config.electrum_node_url.clone(), + tx: hex::encode(tx.serialize()), + }) + .unwrap(); + + println!("Waiting on fulfillment"); + loop { + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + let current = wallet.get_balance().unwrap().confirmed; + // will fail if tx_fees is not an upper bound for real fees. + let tx_fees = 400; + if balance - fee - tx_fees < current { + break; + } + sleep(Duration::from_secs(2)).await; + } +} diff --git a/romeo/tests/tests/bitcoin_client.rs b/romeo/tests/tests/bitcoin_client.rs index f33e6707..8614aacd 100644 --- a/romeo/tests/tests/bitcoin_client.rs +++ b/romeo/tests/tests/bitcoin_client.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor, str::FromStr, thread::sleep, time::Duration}; +use std::{io::Cursor, str::FromStr, time::Duration}; use bdk::{ bitcoin::{hash_types::Txid, Address, BlockHash}, @@ -14,6 +14,7 @@ use blockstack_lib::{ }, }; use romeo::stacks_client::StacksClient; +use tokio::time::sleep; use url::Url; /// devenv's service url @@ -40,7 +41,7 @@ pub fn mine_blocks( .unwrap() } -pub fn wait_for_tx_confirmation( +pub async fn wait_for_tx_confirmation( b_client: &BClient, txid: &Txid, confirmations: i32, @@ -58,7 +59,7 @@ pub fn wait_for_tx_confirmation( } } - sleep(Duration::from_secs(1)); + sleep(Duration::from_secs(1)).await; } } diff --git a/romeo/tests/tests/deposit.rs b/romeo/tests/tests/deposit.rs index 151cf697..242cf056 100644 --- a/romeo/tests/tests/deposit.rs +++ b/romeo/tests/tests/deposit.rs @@ -24,11 +24,12 @@ use super::{ bitcoin_url, client_new, electrs_url, mine_blocks, sbtc_balance, wait_for_tx_confirmation, }, - KeyType::{self, *}, + KeyType::*, WALLETS, }; #[tokio::test] +/// preceeds withdrawal async fn broadcast_deposit() -> Result<()> { let b_client = client_new(bitcoin_url().as_str(), "devnet", "devnet"); @@ -82,8 +83,8 @@ async fn broadcast_deposit() -> Result<()> { } let amount = 10_000; - let deployer_stacks_address = WALLETS[0][KeyType::Stacks].address; - let recipient_stacks_address = WALLETS[1][KeyType::Stacks].address; + let deployer_stacks_address = WALLETS[0][Stacks].address; + let recipient_stacks_address = WALLETS[1][Stacks].address; let tx = { let args = DepositArgs { @@ -131,7 +132,7 @@ async fn broadcast_deposit() -> Result<()> { let txid = tx.txid(); - wait_for_tx_confirmation(&b_client, &txid, 1); + wait_for_tx_confirmation(&b_client, &txid, 1).await; } // assert on new sbtc token balance diff --git a/romeo/tests/tests/withdrawal.rs b/romeo/tests/tests/withdrawal.rs index b7295776..8ece0e84 100644 --- a/romeo/tests/tests/withdrawal.rs +++ b/romeo/tests/tests/withdrawal.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, thread::sleep, time::Duration}; +use std::{str::FromStr, time::Duration}; use bdk::{ bitcoin::{psbt::serialize::Serialize, Address, PrivateKey}, @@ -10,22 +10,53 @@ use bdk::{ template::P2Wpkh, SyncOptions, Wallet, }; +use romeo::{config::Config, stacks_client::StacksClient}; use sbtc_cli::commands::{ broadcast::{broadcast_tx, BroadcastArgs}, withdraw::{build_withdrawal_tx, WithdrawalArgs}, }; +use stacks_core::address::StacksAddress; +use tokio::time::sleep; use super::{ bitcoin_client::{ - bitcoin_url, client_new, electrs_url, mine_blocks, + bitcoin_url, client_new, electrs_url, sbtc_balance, wait_for_tx_confirmation, }, KeyType::*, WALLETS, }; -#[test] -fn broadcast_withdrawal() { +#[tokio::test] +/// Depends on deposit test. +async fn broadcast_withdrawal() { + // wait until stacks addr has some balance + + let deployer_address = + StacksAddress::transmute_stacks_address(WALLETS[0][Stacks].address); + let recipient_address = + StacksAddress::transmute_stacks_address(WALLETS[1][Stacks].address); + + let config = Config::from_path("config.json").unwrap(); + + let stacks_client = + StacksClient::new(config.clone(), reqwest::Client::new()); + + // sbtc credited + let amount = loop { + let amount = sbtc_balance( + &stacks_client, + deployer_address, + recipient_address, + config.contract_name.clone(), + ) + .await; + if amount != 0 { + break amount as u64; + } + sleep(Duration::from_secs(2)).await; + }; + let b_client = client_new(bitcoin_url().as_str(), "devnet", "devnet"); b_client @@ -36,20 +67,50 @@ fn broadcast_withdrawal() { ) .unwrap(); - { - mine_blocks(&b_client, 1, WALLETS[0][P2wpkh].address); - mine_blocks(&b_client, 1, WALLETS[1][P2wpkh].address); - // pads blocks to get rewards. - mine_blocks(&b_client, 100, WALLETS[0][P2wpkh].address); + let args = WithdrawalArgs { + node_url: electrs_url(), + network: bdk::bitcoin::Network::Regtest, + wif: WALLETS[1][P2wpkh].wif.into(), + drawee_wif: WALLETS[1][Stacks].wif.into(), + payee_address: WALLETS[2][P2wpkh].address.into(), + amount, + fulfillment_fee: 2000, + sbtc_wallet: WALLETS[0][P2tr].address.into(), }; - let electrum_url = electrs_url(); + let tx = build_withdrawal_tx(&args).unwrap(); + + broadcast_tx(&BroadcastArgs { + node_url: electrs_url(), + tx: hex::encode(tx.serialize()), + }) + .unwrap(); + + let txid = tx.txid(); + + wait_for_tx_confirmation(&b_client, &txid, 1).await; - // suboptimal, replace once we have better events. + // sbtc debited + { + while { + sbtc_balance( + &stacks_client, + deployer_address, + recipient_address, + config.contract_name.clone(), + ) + } + .await != 0 + { + sleep(Duration::from_secs(2)).await; + } + } + + // btc credited { let blockchain = ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { - url: electrum_url.clone().into(), + url: electrs_url().into(), socks5: None, retry: 3, timeout: Some(10), @@ -58,7 +119,7 @@ fn broadcast_withdrawal() { }) .unwrap(); - let private_key = PrivateKey::from_wif(WALLETS[1][P2wpkh].wif).unwrap(); + let private_key = PrivateKey::from_wif(WALLETS[2][P2wpkh].wif).unwrap(); let wallet = Wallet::new( P2Wpkh(private_key), @@ -68,38 +129,12 @@ fn broadcast_withdrawal() { ) .unwrap(); - loop { + while { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); - let balance = wallet.get_balance().unwrap(); - if balance.confirmed != 0 { - break; - } - sleep(Duration::from_millis(1_000)); + wallet.get_balance().unwrap().confirmed + } == 0 + { + sleep(Duration::from_secs(2)).await; } } - - // amount random [1000,2000) - // fee random [1000,2000) - let args = WithdrawalArgs { - node_url: electrs_url(), - network: bdk::bitcoin::Network::Regtest, - wif: WALLETS[1][P2wpkh].wif.into(), - drawee_wif: WALLETS[1][Stacks].wif.into(), - payee_address: WALLETS[1][P2wpkh].address.into(), - amount: 2000, - fulfillment_fee: 2000, - sbtc_wallet: WALLETS[0][P2tr].address.into(), - }; - - let tx = build_withdrawal_tx(&args).unwrap(); - - broadcast_tx(&BroadcastArgs { - node_url: electrum_url, - tx: hex::encode(tx.serialize()), - }) - .unwrap(); - - let txid = tx.txid(); - - wait_for_tx_confirmation(&b_client, &txid, 1); }