-
-
Save al-f4lc0n/8c34298ddc97dc185f339217a1bd3947 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::error::Error; | |
| use std::str::FromStr; | |
| use std::thread; | |
| use std::time::Duration; | |
| use bitcoin::hex::DisplayHex; | |
| use bitcoin::{absolute, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut}; | |
| use bitcoin::transaction::{Txid, Version}; | |
| use bitcoin::XOnlyPublicKey; | |
| use bitcoincore_rpc::{Client, RpcApi}; | |
| use bitcoincore_rpc::json; | |
| use clarity::types::Address as _; | |
| use clarity::types::chainstate::StacksAddress; | |
| use clarity::vm::types::{PrincipalData, StandardPrincipalData}; | |
| use emily_client::apis::configuration::{ApiKey, Configuration}; | |
| use emily_client::apis::deposit_api; | |
| use emily_client::models; | |
| use sbtc::deposits::{DepositScriptInputs, ReclaimScriptInputs}; | |
| use signer::config::Settings; | |
| use signer::keys::SignerScriptPubKey; | |
| use signer::storage::{postgres::PgStore, DbRead}; | |
| use secp256k1::PublicKey; | |
| pub const DATABASE_URL: &str = "postgres://postgres:postgres@localhost:5432/signer"; | |
| pub const CONFIG_PATH: &str = "signer/src/config/default.toml"; | |
| pub const DEFAULT_STACKS_RECIPIENT: &str = "ST2SBXRBJJTH7GV5J93HJ62W2NRRQ46XYBK92Y039"; | |
| fn to_hex_string(input: &Vec<u8>) -> String { | |
| input.iter().map(|b| format!("{:02x}", b).to_string()).collect::<Vec<String>>().join("") | |
| } | |
| fn get_connection_pool(url: &str) -> sqlx::PgPool { | |
| sqlx::postgres::PgPoolOptions::new() | |
| .max_connections(1) | |
| .min_connections(1) | |
| .acquire_timeout(std::time::Duration::from_secs(5)) | |
| .test_before_acquire(true) | |
| .connect_lazy(url) | |
| .unwrap() | |
| } | |
| async fn get_signers_script_pubkeys() -> Result<String, Box<dyn Error>> { | |
| let db = PgStore::connect(DATABASE_URL).await?; | |
| let signers_script_pubkeys = db.get_signers_script_pubkeys().await?; | |
| Ok(to_hex_string(&signers_script_pubkeys[0])) | |
| } | |
| async fn get_aggregate_key() -> Result<String, Box<dyn Error>> { | |
| let pool = get_connection_pool(DATABASE_URL); | |
| let aggregate_key = sqlx::query_scalar::<_, Vec<u8>>("SELECT aggregate_key FROM sbtc_signer.dkg_shares ORDER BY created_at DESC LIMIT 1;") | |
| .fetch_optional(&pool) | |
| .await? | |
| .unwrap(); | |
| Ok(to_hex_string(&aggregate_key[1..].to_vec())) | |
| } | |
| #[tokio::main] | |
| async fn main() -> Result<(), Box<dyn Error>> { | |
| let settings = Settings::new(Some(CONFIG_PATH))?; | |
| let mut emily_api_endpoint = settings.emily.endpoints.first().unwrap().clone(); | |
| let emily_api_key = if emily_api_endpoint.username().is_empty() { | |
| None | |
| } else { | |
| Some(ApiKey { | |
| prefix: None, | |
| key: emily_api_endpoint.username().to_string(), | |
| }) | |
| }; | |
| let _ = emily_api_endpoint.set_username(""); | |
| let emily_client_config = Configuration { | |
| base_path: emily_api_endpoint.to_string().trim_end_matches("/").to_string(), | |
| api_key: emily_api_key, | |
| ..Default::default() | |
| }; | |
| let bitcoin_url = format!("{}wallet/depositor", settings.bitcoin.rpc_endpoints.first().unwrap()); | |
| let bitcoin_client = Client::new( | |
| &bitcoin_url, | |
| bitcoincore_rpc::Auth::UserPass("devnet".into(), "devnet".into()), | |
| )?; | |
| let aggregate_key = get_aggregate_key().await?; | |
| println!("aggregate_key: {:?}", aggregate_key); | |
| let signers_script_pubkeys = get_signers_script_pubkeys().await?; | |
| println!("signers_script_pubkeys: {:?}", signers_script_pubkeys); | |
| poc5( | |
| &bitcoin_client, | |
| &emily_client_config, | |
| aggregate_key, | |
| ).await?; | |
| Ok(()) | |
| } | |
| async fn poc5( | |
| bitcoin_client: &Client, | |
| emily_config: &Configuration, | |
| aggregate_key: String, | |
| ) -> Result<(), Box<dyn Error>> { | |
| donation(bitcoin_client, aggregate_key.clone(), 40 * 100000000).await?; | |
| create_deposits_btc_and_emily( | |
| bitcoin_client, | |
| emily_config, | |
| aggregate_key, | |
| DEFAULT_STACKS_RECIPIENT.to_string(), | |
| 50, | |
| 10, | |
| 42, | |
| 20000, | |
| false, | |
| ).await?; | |
| println!("┏ Call Emliy to update all deposits to Confirmed"); | |
| let get_deposits_resp = deposit_api::get_deposits( | |
| emily_config, | |
| models::Status::Pending, | |
| None, | |
| None, | |
| ).await?; | |
| let empty_fulfillment = models::Fulfillment::new( | |
| "".to_string(), | |
| 0, | |
| 0, | |
| "".to_string(), | |
| 0, | |
| "".to_string(), | |
| ); | |
| let mut deposit_updates: Vec<models::DepositUpdate> = vec![]; | |
| for pending_deposit in get_deposits_resp.deposits { | |
| deposit_updates.push( | |
| models::DepositUpdate { | |
| bitcoin_tx_output_index: pending_deposit.bitcoin_tx_output_index, | |
| bitcoin_txid: pending_deposit.bitcoin_txid, | |
| fulfillment: Some(Some(Box::new(empty_fulfillment.clone()))), | |
| last_update_block_hash: "".to_string(), | |
| last_update_height: 0, | |
| status: models::Status::Confirmed, | |
| status_message: "".to_string(), | |
| } | |
| ); | |
| } | |
| deposit_api::update_deposits( | |
| emily_config, | |
| models::UpdateDepositsRequestBody::new(deposit_updates), | |
| ).await?; | |
| println!("┗ end"); | |
| Ok(()) | |
| } | |
| async fn wait_btc_tx_confirmed( | |
| bitcoin_client: &Client, | |
| txid: &Txid, | |
| ) -> Result<(), Box<dyn Error>> { | |
| println!("┃ wait for tx {:?} to be confirmed ...", txid); | |
| loop { | |
| println!("┃ ..."); | |
| let tx = bitcoin_client.get_transaction(txid, None)?; | |
| if !tx.info.blockhash.is_none() { | |
| break; | |
| } | |
| thread::sleep(Duration::from_secs(10)); | |
| } | |
| println!("┃ confirmed!"); | |
| Ok(()) | |
| } | |
| async fn create_deposits_btc_and_emily( | |
| bitcoin_client: &Client, | |
| emily_config: &Configuration, | |
| aggregate_key: String, | |
| stacks_recipient: String, | |
| lock_time: u32, | |
| number_of_deposits: u64, | |
| amount_per_deposit: u64, | |
| max_fee_per_deposit: u64, | |
| wait_tx_confirmed: bool, | |
| ) -> Result<(), Box<dyn Error>> { | |
| println!("┏ create_deposits_btc_and_emily"); | |
| println!("┃ tx number of deposits: {:?}", number_of_deposits); | |
| let (unsigned_tx, deposit_script, reclaim_script) = create_bitcoin_deposit_transaction_multi_deposit( | |
| &bitcoin_client, | |
| aggregate_key, | |
| stacks_recipient, | |
| lock_time, | |
| number_of_deposits, | |
| amount_per_deposit, | |
| max_fee_per_deposit, | |
| )?; | |
| println!("┃ deposit script: {:?}", deposit_script.deposit_script().as_bytes().to_lower_hex_string()); | |
| println!("┃ reclaim script: {:?}", reclaim_script.reclaim_script().as_bytes().to_lower_hex_string()); | |
| let signed_tx = bitcoin_client.sign_raw_transaction_with_wallet(&unsigned_tx, None, None)?; | |
| // println!("Signed transaction: {:?}", hex::encode(&signed_tx.hex)); | |
| let txid = bitcoin_client.send_raw_transaction(&signed_tx.hex)?; | |
| assert_eq!(txid, unsigned_tx.compute_txid()); | |
| if wait_tx_confirmed { | |
| wait_btc_tx_confirmed(bitcoin_client, &txid).await?; | |
| } | |
| println!("┃ call emily to create all deposits"); | |
| for i in 0..number_of_deposits { | |
| let _ = deposit_api::create_deposit( | |
| emily_config, | |
| models::CreateDepositRequestBody { | |
| bitcoin_tx_output_index: i as u32, | |
| bitcoin_txid: txid.to_string(), | |
| deposit_script: deposit_script.deposit_script().to_hex_string(), | |
| reclaim_script: reclaim_script.reclaim_script().to_hex_string(), | |
| }, | |
| ) | |
| .await?; | |
| } | |
| println!("┗ end"); | |
| Ok(()) | |
| } | |
| async fn donation( | |
| bitcoin_client: &Client, | |
| aggregate_key: String, | |
| donation_amount: u64, | |
| ) -> Result<(), Box<dyn Error>> { | |
| println!("┏ donation to aggregate_key"); | |
| println!("┃ donation amount: {:?}", donation_amount); | |
| let opts = json::ListUnspentQueryOptions { | |
| minimum_amount: Some(Amount::from_sat(donation_amount)), | |
| ..Default::default() | |
| }; | |
| let unspent = bitcoin_client | |
| .list_unspent(Some(6), None, None, None, Some(opts))? | |
| .into_iter() | |
| .next() | |
| .ok_or("list_unspent error")?; | |
| let pubkey = XOnlyPublicKey::from_str(&aggregate_key) | |
| .or_else(|_| PublicKey::from_str(&aggregate_key).map(XOnlyPublicKey::from))?; | |
| let relay_fee = 200; | |
| let unsigned_tx = Transaction { | |
| input: vec![TxIn { | |
| previous_output: OutPoint { | |
| txid: unspent.txid, | |
| vout: unspent.vout, | |
| }, | |
| script_sig: Default::default(), | |
| sequence: Sequence::ZERO, | |
| witness: Default::default(), | |
| }], | |
| output: vec![ | |
| TxOut { | |
| value: Amount::from_sat(donation_amount), | |
| script_pubkey: pubkey.signers_script_pubkey(), | |
| }, | |
| TxOut { | |
| value: Amount::from_sat(unspent.amount.to_sat() - donation_amount - relay_fee), | |
| script_pubkey: unspent.script_pub_key, // Return the remaining BTC to unspent UTXO public key | |
| }, | |
| ], | |
| version: Version::TWO, | |
| lock_time: absolute::LockTime::ZERO, | |
| }; | |
| let signed_tx = bitcoin_client.sign_raw_transaction_with_wallet(&unsigned_tx, None, None)?; | |
| let txid = bitcoin_client.send_raw_transaction(&signed_tx.hex)?; | |
| wait_btc_tx_confirmed(bitcoin_client, &txid).await?; | |
| println!("┗ end"); | |
| Ok(()) | |
| } | |
| fn create_bitcoin_deposit_transaction_multi_deposit( | |
| bitcoin_client: &Client, | |
| aggregate_key: String, | |
| stacks_recipient: String, | |
| lock_time: u32, | |
| number_of_deposits: u64, | |
| amount_per_deposit: u64, | |
| max_fee_per_deposit: u64, | |
| ) -> Result<(Transaction, DepositScriptInputs, ReclaimScriptInputs), Box<dyn Error>> { | |
| let total_depsites_spend = (amount_per_deposit + max_fee_per_deposit) * number_of_deposits; | |
| let opts = json::ListUnspentQueryOptions { | |
| minimum_amount: Some(Amount::from_sat(total_depsites_spend)), | |
| ..Default::default() | |
| }; | |
| let unspent = bitcoin_client | |
| .list_unspent(Some(6), None, None, None, Some(opts))? | |
| .into_iter() | |
| .next() | |
| .ok_or("list_unspent error")?; | |
| let pubkey = XOnlyPublicKey::from_str(&aggregate_key) | |
| .or_else(|_| PublicKey::from_str(&aggregate_key).map(XOnlyPublicKey::from))?; | |
| let deposit_script = DepositScriptInputs { | |
| signers_public_key: pubkey, | |
| max_fee: max_fee_per_deposit, | |
| recipient: PrincipalData::Standard(StandardPrincipalData::from( | |
| StacksAddress::from_string(&stacks_recipient).ok_or("StacksAddress::from_string error")?, | |
| )), | |
| }; | |
| let reclaim_script = ReclaimScriptInputs::try_new(lock_time, ScriptBuf::new())?; | |
| let mut output: Vec<TxOut> = vec![]; | |
| for _ in 0..number_of_deposits { | |
| output.push(TxOut { | |
| value: Amount::from_sat(amount_per_deposit + max_fee_per_deposit), | |
| script_pubkey: sbtc::deposits::to_script_pubkey( | |
| deposit_script.deposit_script(), | |
| reclaim_script.reclaim_script(), | |
| ), | |
| }); | |
| } | |
| let relay_fee = 500+50*number_of_deposits; | |
| output.push(TxOut { | |
| value: Amount::from_sat(unspent.amount.to_sat() - total_depsites_spend - relay_fee), | |
| script_pubkey: unspent.script_pub_key, // Return the remaining BTC to unspent UTXO public key | |
| }); | |
| let unsigned_tx = Transaction { | |
| input: vec![TxIn { | |
| previous_output: OutPoint { | |
| txid: unspent.txid, | |
| vout: unspent.vout, | |
| }, | |
| script_sig: Default::default(), | |
| sequence: Sequence::ZERO, | |
| witness: Default::default(), | |
| }], | |
| output: output, | |
| version: Version::TWO, | |
| lock_time: absolute::LockTime::ZERO, | |
| }; | |
| Ok((unsigned_tx, deposit_script, reclaim_script)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment