From 7578c99c4df735d3a975a752216f1bf765738042 Mon Sep 17 00:00:00 2001 From: raph Date: Tue, 9 Apr 2024 21:53:27 +0200 Subject: [PATCH] Add wallet batch outputs and inscriptions endpoints (#3456) --- src/api.rs | 8 +- src/index.rs | 134 +++++++++++++---- src/subcommand/server.rs | 169 ++++++++------------- src/subcommand/wallet.rs | 4 +- src/wallet.rs | 247 +------------------------------ src/wallet/wallet_constructor.rs | 231 +++++++++++++++++++++++++++++ 6 files changed, 405 insertions(+), 388 deletions(-) create mode 100644 src/wallet/wallet_constructor.rs diff --git a/src/api.rs b/src/api.rs index 0983b5225e..89a124b9da 100644 --- a/src/api.rs +++ b/src/api.rs @@ -136,7 +136,7 @@ impl Output { chain: Chain, inscriptions: Vec, outpoint: OutPoint, - output: TxOut, + tx_out: TxOut, indexed: bool, runes: Vec<(SpacedRune, Pile)>, sat_ranges: Option>, @@ -144,17 +144,17 @@ impl Output { ) -> Self { Self { address: chain - .address_from_script(&output.script_pubkey) + .address_from_script(&tx_out.script_pubkey) .ok() .map(|address| uncheck(&address)), indexed, inscriptions, runes, sat_ranges, - script_pubkey: output.script_pubkey.to_asm_string(), + script_pubkey: tx_out.script_pubkey.to_asm_string(), spent, transaction: outpoint.txid.to_string(), - value: output.value, + value: tx_out.value, } } } diff --git a/src/index.rs b/src/index.rs index bec84d0a25..0ce7f33203 100644 --- a/src/index.rs +++ b/src/index.rs @@ -157,19 +157,6 @@ pub(crate) struct TransactionInfo { pub(crate) starting_timestamp: u128, } -pub(crate) struct InscriptionInfo { - pub(crate) children: Vec, - pub(crate) entry: InscriptionEntry, - pub(crate) parents: Vec, - pub(crate) output: Option, - pub(crate) satpoint: SatPoint, - pub(crate) inscription: Inscription, - pub(crate) previous: Option, - pub(crate) next: Option, - pub(crate) rune: Option, - pub(crate) charms: u16, -} - pub(crate) trait BitcoinCoreRpcResultExt { fn into_option(self) -> Result>; } @@ -1801,15 +1788,11 @@ impl Index { ) } - pub fn inscription_info_benchmark(index: &Index, inscription_number: i32) { - Self::inscription_info(index, query::Inscription::Number(inscription_number)).unwrap(); - } - pub(crate) fn inscription_info( - index: &Index, + &self, query: query::Inscription, - ) -> Result> { - let rtx = index.database.begin_read()?; + ) -> Result, Inscription)>> { + let rtx = self.database.begin_read()?; let sequence_number = match query { query::Inscription::Id(id) => rtx @@ -1842,7 +1825,7 @@ impl Index { .value(), ); - let Some(transaction) = index.get_transaction(entry.id.txid)? else { + let Some(transaction) = self.get_transaction(entry.id.txid)? else { return Ok(None); }; @@ -1866,7 +1849,7 @@ impl Index { { None } else { - let Some(transaction) = index.get_transaction(satpoint.outpoint.txid)? else { + let Some(transaction) = self.get_transaction(satpoint.outpoint.txid)? else { return Ok(None); }; @@ -1943,18 +1926,50 @@ impl Index { Charm::Lost.set(&mut charms); } - Ok(Some(InscriptionInfo { - children, - entry, - parents, + let effective_mime_type = if let Some(delegate_id) = inscription.delegate() { + let delegate_result = self.get_inscription_by_id(delegate_id); + if let Ok(Some(delegate)) = delegate_result { + delegate.content_type().map(str::to_string) + } else { + inscription.content_type().map(str::to_string) + } + } else { + inscription.content_type().map(str::to_string) + }; + + Ok(Some(( + api::Inscription { + address: output + .as_ref() + .and_then(|o| { + self + .settings + .chain() + .address_from_script(&o.script_pubkey) + .ok() + }) + .map(|address| address.to_string()), + charms: Charm::charms(charms), + children, + content_length: inscription.content_length(), + content_type: inscription.content_type().map(|s| s.to_string()), + effective_content_type: effective_mime_type, + fee: entry.fee, + height: entry.height, + id: entry.id, + next, + number: entry.inscription_number, + parents, + previous, + rune, + sat: entry.sat, + satpoint, + timestamp: timestamp(entry.timestamp.into()).timestamp(), + value: output.as_ref().map(|o| o.value), + }, output, - satpoint, inscription, - previous, - next, - rune, - charms, - })) + ))) } pub(crate) fn get_inscription_entry( @@ -2104,6 +2119,61 @@ impl Index { .collect(), ) } + + pub(crate) fn get_output_info(&self, outpoint: OutPoint) -> Result> { + let sat_ranges = self.list(outpoint)?; + + let indexed; + + let txout = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { + let mut value = 0; + + if let Some(ranges) = &sat_ranges { + for (start, end) in ranges { + value += end - start; + } + } + + indexed = true; + + TxOut { + value, + script_pubkey: ScriptBuf::new(), + } + } else { + indexed = self.contains_output(&outpoint)?; + + let Some(tx) = self.get_transaction(outpoint.txid)? else { + return Ok(None); + }; + + let Some(output) = tx.output.into_iter().nth(outpoint.vout as usize) else { + return Ok(None); + }; + + output + }; + + let inscriptions = self.get_inscriptions_on_output(outpoint)?; + + let runes = self.get_rune_balances_for_outpoint(outpoint)?; + + let spent = self.is_output_spent(outpoint)?; + + Ok(Some(( + api::Output::new( + self.settings.chain(), + inscriptions, + outpoint, + txout.clone(), + indexed, + runes, + sat_ranges, + spent, + ), + txout, + ))) + } } #[cfg(test)] diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 26217801ae..f24563dafc 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -18,7 +18,7 @@ use { extract::{Extension, Json, Path, Query}, http::{header, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, - routing::get, + routing::{get, post}, Router, }, axum_server::Handle, @@ -204,6 +204,7 @@ impl Server { .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) + .route("/inscriptions", post(Self::inscriptions_json)) .route("/inscriptions/:page", get(Self::inscriptions_paginated)) .route( "/inscriptions/block/:height", @@ -216,6 +217,7 @@ impl Server { .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) + .route("/outputs", post(Self::outputs)) .route("/parents/:inscription_id", get(Self::parents)) .route( "/parents/:inscription_id/:page", @@ -579,64 +581,21 @@ impl Server { AcceptJson(accept_json): AcceptJson, ) -> ServerResult { task::block_in_place(|| { - let sat_ranges = index.list(outpoint)?; - - let indexed; - - let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { - let mut value = 0; - - if let Some(ranges) = &sat_ranges { - for (start, end) in ranges { - value += end - start; - } - } - - indexed = true; - - TxOut { - value, - script_pubkey: ScriptBuf::new(), - } - } else { - indexed = index.contains_output(&outpoint)?; - - index - .get_transaction(outpoint.txid)? - .ok_or_not_found(|| format!("output {outpoint}"))? - .output - .into_iter() - .nth(outpoint.vout as usize) - .ok_or_not_found(|| format!("output {outpoint}"))? - }; - - let inscriptions = index.get_inscriptions_on_output(outpoint)?; - - let runes = index.get_rune_balances_for_outpoint(outpoint)?; - - let spent = index.is_output_spent(outpoint)?; + let (output_info, txout) = index + .get_output_info(outpoint)? + .ok_or_not_found(|| format!("output {outpoint}"))?; Ok(if accept_json { - Json(api::Output::new( - server_config.chain, - inscriptions, - outpoint, - output, - indexed, - runes, - sat_ranges, - spent, - )) - .into_response() + Json(output_info).into_response() } else { OutputHtml { chain: server_config.chain, - inscriptions, + inscriptions: output_info.inscriptions, outpoint, - output, - runes, - sat_ranges, - spent, + output: txout, + runes: output_info.runes, + sat_ranges: output_info.sat_ranges, + spent: output_info.spent, } .page(server_config) .into_response() @@ -644,6 +603,24 @@ impl Server { }) } + async fn outputs( + Extension(index): Extension>, + _: AcceptJson, + Json(outputs): Json>, + ) -> ServerResult { + task::block_in_place(|| { + let mut response = Vec::new(); + for outpoint in outputs { + let (output_info, _) = index + .get_output_info(outpoint)? + .ok_or_not_found(|| format!("output {outpoint}"))?; + + response.push(output_info); + } + Ok(Json(response).into_response()) + }) + } + async fn range( Extension(server_config): Extension>, Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<( @@ -1516,69 +1493,33 @@ impl Server { } } - let info = Index::inscription_info(&index, query)? + let (info, txout, inscription) = index + .inscription_info(query)? .ok_or_not_found(|| format!("inscription {query}"))?; - let effective_mime_type = if let Some(delegate_id) = info.inscription.delegate() { - let delegate_result = index.get_inscription_by_id(delegate_id); - if let Ok(Some(delegate)) = delegate_result { - delegate.content_type().map(str::to_string) - } else { - info.inscription.content_type().map(str::to_string) - } - } else { - info.inscription.content_type().map(str::to_string) - }; - Ok(if accept_json { - Json(api::Inscription { - address: info - .output - .as_ref() - .and_then(|o| { - server_config - .chain - .address_from_script(&o.script_pubkey) - .ok() - }) - .map(|address| address.to_string()), - charms: Charm::charms(info.charms), - children: info.children, - content_length: info.inscription.content_length(), - content_type: info.inscription.content_type().map(|s| s.to_string()), - effective_content_type: effective_mime_type, - fee: info.entry.fee, - height: info.entry.height, - id: info.entry.id, - next: info.next, - number: info.entry.inscription_number, - parents: info.parents, - previous: info.previous, - rune: info.rune, - sat: info.entry.sat, - satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp.into()).timestamp(), - value: info.output.as_ref().map(|o| o.value), - }) - .into_response() + Json(info).into_response() } else { InscriptionHtml { chain: server_config.chain, - charms: Charm::Vindicated.unset(info.charms), + charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| { + charm.set(&mut acc); + acc + })), children: info.children, - fee: info.entry.fee, - height: info.entry.height, - inscription: info.inscription, - id: info.entry.id, - number: info.entry.inscription_number, + fee: info.fee, + height: info.height, + inscription, + id: info.id, + number: info.number, next: info.next, - output: info.output, + output: txout, parents: info.parents, previous: info.previous, rune: info.rune, - sat: info.entry.sat, + sat: info.sat, satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp.into()), + timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(), } .page(server_config) .into_response() @@ -1586,6 +1527,26 @@ impl Server { }) } + async fn inscriptions_json( + Extension(index): Extension>, + _: AcceptJson, + Json(inscriptions): Json>, + ) -> ServerResult { + task::block_in_place(|| { + let mut response = Vec::new(); + for inscription in inscriptions { + let query = query::Inscription::Id(inscription); + let (info, _, _) = index + .inscription_info(query)? + .ok_or_not_found(|| format!("inscription {query}"))?; + + response.push(info); + } + + Ok(Json(response).into_response()) + }) + } + async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 5a2b55b18f..0c713df8cd 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,6 +1,6 @@ use { super::*, - crate::wallet::{batch, Wallet}, + crate::wallet::{batch, wallet_constructor::WalletConstructor, Wallet}, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, shared_args::SharedArgs, }; @@ -80,7 +80,7 @@ impl WalletCommand { _ => {} }; - let wallet = Wallet::build( + let wallet = WalletConstructor::construct( self.name.clone(), self.no_sync, settings.clone(), diff --git a/src/wallet.rs b/src/wallet.rs index a524e9f763..0151376f5c 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -10,10 +10,6 @@ use { bitcoincore_rpc::bitcoincore_rpc_json::{Descriptor, ImportDescriptors, Timestamp}, entry::{EtchingEntry, EtchingEntryValue}, fee_rate::FeeRate, - futures::{ - future::{self, FutureExt}, - try_join, TryFutureExt, - }, index::entry::Entry, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, @@ -27,6 +23,7 @@ use { pub mod batch; pub mod entry; pub mod transaction_builder; +pub mod wallet_constructor; const SCHEMA_VERSION: u64 = 1; @@ -50,23 +47,6 @@ impl From for u64 { } } -#[derive(Clone)] -struct OrdClient { - url: Url, - client: reqwest::Client, -} - -impl OrdClient { - pub async fn get(&self, path: &str) -> Result { - self - .client - .get(self.url.join(path)?) - .send() - .map_err(|err| anyhow!(err)) - .await - } -} - pub(crate) struct Wallet { bitcoin_client: Client, database: Database, @@ -83,231 +63,6 @@ pub(crate) struct Wallet { } impl Wallet { - pub(crate) fn build( - name: String, - no_sync: bool, - settings: Settings, - rpc_url: Url, - ) -> Result { - let mut headers = HeaderMap::new(); - - headers.insert( - header::ACCEPT, - header::HeaderValue::from_static("application/json"), - ); - - if let Some((username, password)) = settings.credentials() { - let credentials = - base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}")); - headers.insert( - header::AUTHORIZATION, - header::HeaderValue::from_str(&format!("Basic {credentials}")).unwrap(), - ); - } - - let database = Self::open_database(&name, &settings)?; - - let ord_client = reqwest::blocking::ClientBuilder::new() - .default_headers(headers.clone()) - .build()?; - - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()? - .block_on(async move { - let bitcoin_client = { - let client = Self::check_version(settings.bitcoin_rpc_client(Some(name.clone()))?)?; - - if !client.list_wallets()?.contains(&name) { - client.load_wallet(&name)?; - } - - Self::check_descriptors(&name, client.list_descriptors(None)?.descriptors)?; - - client - }; - - let async_ord_client = OrdClient { - url: rpc_url.clone(), - client: reqwest::ClientBuilder::new() - .default_headers(headers.clone()) - .build()?, - }; - - let chain_block_count = bitcoin_client.get_block_count().unwrap() + 1; - - if !no_sync { - for i in 0.. { - let response = async_ord_client.get("/blockcount").await?; - if response - .text() - .await? - .parse::() - .expect("wallet failed to talk to server. Make sure `ord server` is running.") - >= chain_block_count - { - break; - } else if i == 20 { - bail!("wallet failed to synchronize with `ord server` after {i} attempts"); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } - - let mut utxos = Self::get_utxos(&bitcoin_client)?; - let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?; - utxos.extend(locked_utxos.clone()); - - let requests = utxos - .clone() - .into_keys() - .map(|output| (output, Self::get_output(&async_ord_client, output))) - .collect::>(); - - let futures = requests.into_iter().map(|(output, req)| async move { - let result = req.await; - (output, result) - }); - - let results = future::join_all(futures).await; - - let mut output_info = BTreeMap::new(); - for (output, result) in results { - let info = result?; - output_info.insert(output, info); - } - - let requests = output_info - .iter() - .flat_map(|(_output, info)| info.inscriptions.clone()) - .collect::>() - .into_iter() - .map(|id| (id, Self::get_inscription_info(&async_ord_client, id))) - .collect::>(); - - let futures = requests.into_iter().map(|(output, req)| async move { - let result = req.await; - (output, result) - }); - - let (results, status) = try_join!( - future::join_all(futures).map(Ok), - Self::get_server_status(&async_ord_client) - )?; - - let mut inscriptions = BTreeMap::new(); - let mut inscription_info = BTreeMap::new(); - for (id, result) in results { - let info = result?; - inscriptions - .entry(info.satpoint) - .or_insert_with(Vec::new) - .push(id); - - inscription_info.insert(id, info); - } - - Ok(Wallet { - bitcoin_client, - database, - has_rune_index: status.rune_index, - has_sat_index: status.sat_index, - inscription_info, - inscriptions, - locked_utxos, - ord_client, - output_info, - rpc_url, - settings, - utxos, - }) - }) - } - - async fn get_output(ord_client: &OrdClient, output: OutPoint) -> Result { - let response = ord_client.get(&format!("/output/{output}")).await?; - - if !response.status().is_success() { - bail!("wallet failed get output: {}", response.text().await?); - } - - let output_json: api::Output = serde_json::from_str(&response.text().await?)?; - - if !output_json.indexed { - bail!("output in wallet but not in ord server: {output}"); - } - - Ok(output_json) - } - - fn get_utxos(bitcoin_client: &Client) -> Result> { - Ok( - bitcoin_client - .list_unspent(None, None, None, None, None)? - .into_iter() - .map(|utxo| { - let outpoint = OutPoint::new(utxo.txid, utxo.vout); - let txout = TxOut { - script_pubkey: utxo.script_pub_key, - value: utxo.amount.to_sat(), - }; - - (outpoint, txout) - }) - .collect(), - ) - } - - fn get_locked_utxos(bitcoin_client: &Client) -> Result> { - #[derive(Deserialize)] - pub(crate) struct JsonOutPoint { - txid: Txid, - vout: u32, - } - - let outpoints = bitcoin_client.call::>("listlockunspent", &[])?; - - let mut utxos = BTreeMap::new(); - - for outpoint in outpoints { - let txout = bitcoin_client - .get_raw_transaction(&outpoint.txid, None)? - .output - .get(TryInto::::try_into(outpoint.vout).unwrap()) - .cloned() - .ok_or_else(|| anyhow!("Invalid output index"))?; - - utxos.insert(OutPoint::new(outpoint.txid, outpoint.vout), txout); - } - - Ok(utxos) - } - - async fn get_inscription_info( - ord_client: &OrdClient, - inscription_id: InscriptionId, - ) -> Result { - let response = ord_client - .get(&format!("/inscription/{inscription_id}")) - .await?; - - if !response.status().is_success() { - bail!("inscription {inscription_id} not found"); - } - - Ok(serde_json::from_str(&response.text().await?)?) - } - - async fn get_server_status(ord_client: &OrdClient) -> Result { - let response = ord_client.get("/status").await?; - - if !response.status().is_success() { - bail!("could not get status: {}", response.text().await?) - } - - Ok(serde_json::from_str(&response.text().await?)?) - } - pub(crate) fn get_output_sat_ranges(&self) -> Result)>> { ensure!( self.has_sat_index, diff --git a/src/wallet/wallet_constructor.rs b/src/wallet/wallet_constructor.rs new file mode 100644 index 0000000000..f485f6f8fb --- /dev/null +++ b/src/wallet/wallet_constructor.rs @@ -0,0 +1,231 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct WalletConstructor { + ord_client: reqwest::blocking::Client, + name: String, + no_sync: bool, + rpc_url: Url, + settings: Settings, +} + +impl WalletConstructor { + pub(crate) fn construct( + name: String, + no_sync: bool, + settings: Settings, + rpc_url: Url, + ) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + header::ACCEPT, + header::HeaderValue::from_static("application/json"), + ); + + if let Some((username, password)) = settings.credentials() { + let credentials = + base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}")); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Basic {credentials}")).unwrap(), + ); + } + + Self { + ord_client: reqwest::blocking::ClientBuilder::new() + .default_headers(headers.clone()) + .build()?, + name, + no_sync, + rpc_url, + settings, + } + .build() + } + + pub(crate) fn build(self) -> Result { + let database = Wallet::open_database(&self.name, &self.settings)?; + + let bitcoin_client = { + let client = + Wallet::check_version(self.settings.bitcoin_rpc_client(Some(self.name.clone()))?)?; + + if !client.list_wallets()?.contains(&self.name) { + client.load_wallet(&self.name)?; + } + + Wallet::check_descriptors(&self.name, client.list_descriptors(None)?.descriptors)?; + + client + }; + + let chain_block_count = bitcoin_client.get_block_count().unwrap() + 1; + + if !self.no_sync { + for i in 0.. { + let response = self.get("/blockcount")?; + + if response + .text()? + .parse::() + .expect("wallet failed to talk to server. Make sure `ord server` is running.") + >= chain_block_count + { + break; + } else if i == 20 { + bail!("wallet failed to synchronize with `ord server` after {i} attempts"); + } + std::thread::sleep(Duration::from_millis(50)); + } + } + + let mut utxos = Self::get_utxos(&bitcoin_client)?; + let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?; + utxos.extend(locked_utxos.clone()); + + let output_info = self.get_output_info(utxos.clone().into_keys().collect())?; + + let inscriptions = output_info + .iter() + .flat_map(|(_output, info)| info.inscriptions.clone()) + .collect::>(); + + let (inscriptions, inscription_info) = self.get_inscriptions(&inscriptions)?; + + let status = self.get_server_status()?; + + Ok(Wallet { + bitcoin_client, + database, + has_rune_index: status.rune_index, + has_sat_index: status.sat_index, + inscription_info, + inscriptions, + locked_utxos, + ord_client: self.ord_client, + output_info, + rpc_url: self.rpc_url, + settings: self.settings, + utxos, + }) + } + + fn get_output_info(&self, outputs: Vec) -> Result> { + let response = self.post("/outputs", &outputs)?; + + if !response.status().is_success() { + bail!("wallet failed get outputs: {}", response.text()?); + } + + let output_info: BTreeMap = outputs + .into_iter() + .zip(serde_json::from_str::>(&response.text()?)?) + .collect(); + + for (output, info) in &output_info { + if !info.indexed { + bail!("output in wallet but not in ord server: {output}"); + } + } + + Ok(output_info) + } + + fn get_inscriptions( + &self, + inscriptions: &Vec, + ) -> Result<( + BTreeMap>, + BTreeMap, + )> { + let response = self.post("/inscriptions", inscriptions)?; + + if !response.status().is_success() { + bail!("wallet failed get inscriptions: {}", response.text()?); + } + + let mut inscriptions = BTreeMap::new(); + let mut inscription_infos = BTreeMap::new(); + for info in serde_json::from_str::>(&response.text()?)? { + inscriptions + .entry(info.satpoint) + .or_insert_with(Vec::new) + .push(info.id); + + inscription_infos.insert(info.id, info); + } + + Ok((inscriptions, inscription_infos)) + } + + fn get_utxos(bitcoin_client: &Client) -> Result> { + Ok( + bitcoin_client + .list_unspent(None, None, None, None, None)? + .into_iter() + .map(|utxo| { + let outpoint = OutPoint::new(utxo.txid, utxo.vout); + let txout = TxOut { + script_pubkey: utxo.script_pub_key, + value: utxo.amount.to_sat(), + }; + + (outpoint, txout) + }) + .collect(), + ) + } + + fn get_locked_utxos(bitcoin_client: &Client) -> Result> { + #[derive(Deserialize)] + pub(crate) struct JsonOutPoint { + txid: Txid, + vout: u32, + } + + let outpoints = bitcoin_client.call::>("listlockunspent", &[])?; + + let mut utxos = BTreeMap::new(); + + for outpoint in outpoints { + let txout = bitcoin_client + .get_raw_transaction(&outpoint.txid, None)? + .output + .get(TryInto::::try_into(outpoint.vout).unwrap()) + .cloned() + .ok_or_else(|| anyhow!("Invalid output index"))?; + + utxos.insert(OutPoint::new(outpoint.txid, outpoint.vout), txout); + } + + Ok(utxos) + } + + fn get_server_status(&self) -> Result { + let response = self.get("/status")?; + + if !response.status().is_success() { + bail!("could not get status: {}", response.text()?) + } + + Ok(serde_json::from_str(&response.text()?)?) + } + + pub fn get(&self, path: &str) -> Result { + self + .ord_client + .get(self.rpc_url.join(path)?) + .send() + .map_err(|err| anyhow!(err)) + } + + pub fn post(&self, path: &str, body: &impl Serialize) -> Result { + self + .ord_client + .post(self.rpc_url.join(path)?) + .json(body) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .map_err(|err| anyhow!(err)) + } +}