feat(rpc): Implement `getaddressutxos` RPC method. (#4087)
* implement display for `Script` * implement `getaddressutxos` * fix space * normalize list of addresses as argument to rpc methods * implement `AddressStrings` * make a doc clearer Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
e90c22917d
commit
56aabb1db1
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use hex::ToHex;
|
||||||
|
|
||||||
use crate::serialization::{
|
use crate::serialization::{
|
||||||
zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashSerialize,
|
zcash_serialize_bytes, SerializationError, ZcashDeserialize, ZcashSerialize,
|
||||||
};
|
};
|
||||||
|
|
@ -40,6 +42,12 @@ impl Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Script {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str(&self.encode_hex::<String>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Script {
|
impl fmt::Debug for Script {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_tuple("Script")
|
f.debug_tuple("Script")
|
||||||
|
|
@ -48,6 +56,26 @@ impl fmt::Debug for Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToHex for &Script {
|
||||||
|
fn encode_hex<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.as_raw_bytes().encode_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
|
||||||
|
self.as_raw_bytes().encode_hex_upper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToHex for Script {
|
||||||
|
fn encode_hex<T: FromIterator<char>>(&self) -> T {
|
||||||
|
(&self).encode_hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
|
||||||
|
(&self).encode_hex_upper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ZcashSerialize for Script {
|
impl ZcashSerialize for Script {
|
||||||
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
|
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
|
||||||
zcash_serialize_bytes(&self.0, writer)
|
zcash_serialize_bytes(&self.0, writer)
|
||||||
|
|
|
||||||
|
|
@ -190,10 +190,28 @@ pub trait Rpc {
|
||||||
#[rpc(name = "getaddresstxids")]
|
#[rpc(name = "getaddresstxids")]
|
||||||
fn get_address_tx_ids(
|
fn get_address_tx_ids(
|
||||||
&self,
|
&self,
|
||||||
addresses: Vec<String>,
|
address_strings: AddressStrings,
|
||||||
start: u32,
|
start: u32,
|
||||||
end: u32,
|
end: u32,
|
||||||
) -> BoxFuture<Result<Vec<String>>>;
|
) -> BoxFuture<Result<Vec<String>>>;
|
||||||
|
|
||||||
|
/// Returns all unspent outputs for a list of addresses.
|
||||||
|
///
|
||||||
|
/// zcashd reference: [`getaddressutxos`](https://zcash.github.io/rpc/getaddressutxos.html)
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// - `addresses`: (json array of string, required) The addresses to get outputs from.
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
///
|
||||||
|
/// lightwalletd always uses the multi-address request, without chaininfo:
|
||||||
|
/// https://github.com/zcash/lightwalletd/blob/master/frontend/service.go#L402
|
||||||
|
#[rpc(name = "getaddressutxos")]
|
||||||
|
fn get_address_utxos(
|
||||||
|
&self,
|
||||||
|
address_strings: AddressStrings,
|
||||||
|
) -> BoxFuture<Result<Vec<GetAddressUtxos>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RPC method implementations.
|
/// RPC method implementations.
|
||||||
|
|
@ -403,17 +421,9 @@ where
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let addresses: HashSet<Address> = address_strings
|
let valid_addresses = address_strings.valid_addresses()?;
|
||||||
.addresses
|
|
||||||
.into_iter()
|
|
||||||
.map(|address| {
|
|
||||||
address.parse().map_err(|error| {
|
|
||||||
Error::invalid_params(&format!("invalid address {address:?}: {error}"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<_>>()?;
|
|
||||||
|
|
||||||
let request = zebra_state::ReadRequest::AddressBalance(addresses);
|
let request = zebra_state::ReadRequest::AddressBalance(valid_addresses);
|
||||||
let response = state.oneshot(request).await.map_err(|error| Error {
|
let response = state.oneshot(request).await.map_err(|error| Error {
|
||||||
code: ErrorCode::ServerError(0),
|
code: ErrorCode::ServerError(0),
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
|
|
@ -642,7 +652,7 @@ where
|
||||||
|
|
||||||
fn get_address_tx_ids(
|
fn get_address_tx_ids(
|
||||||
&self,
|
&self,
|
||||||
addresses: Vec<String>,
|
address_strings: AddressStrings,
|
||||||
start: u32,
|
start: u32,
|
||||||
end: u32,
|
end: u32,
|
||||||
) -> BoxFuture<Result<Vec<String>>> {
|
) -> BoxFuture<Result<Vec<String>>> {
|
||||||
|
|
@ -660,17 +670,10 @@ where
|
||||||
// height range checks
|
// height range checks
|
||||||
check_height_range(start, end, chain_height?)?;
|
check_height_range(start, end, chain_height?)?;
|
||||||
|
|
||||||
let valid_addresses: Result<HashSet<Address>> = addresses
|
let valid_addresses = address_strings.valid_addresses()?;
|
||||||
.iter()
|
|
||||||
.map(|address| {
|
|
||||||
address.parse().map_err(|_| {
|
|
||||||
Error::invalid_params(format!("Provided address is not valid: {}", address))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let request = zebra_state::ReadRequest::TransactionIdsByAddresses {
|
let request = zebra_state::ReadRequest::TransactionIdsByAddresses {
|
||||||
addresses: valid_addresses?,
|
addresses: valid_addresses,
|
||||||
height_range: start..=end,
|
height_range: start..=end,
|
||||||
};
|
};
|
||||||
let response = state
|
let response = state
|
||||||
|
|
@ -694,6 +697,56 @@ where
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_address_utxos(
|
||||||
|
&self,
|
||||||
|
address_strings: AddressStrings,
|
||||||
|
) -> BoxFuture<Result<Vec<GetAddressUtxos>>> {
|
||||||
|
let mut state = self.state.clone();
|
||||||
|
let mut response_utxos = vec![];
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let valid_addresses = address_strings.valid_addresses()?;
|
||||||
|
|
||||||
|
// get utxos data for addresses
|
||||||
|
let request = zebra_state::ReadRequest::UtxosByAddresses(valid_addresses);
|
||||||
|
let response = state
|
||||||
|
.ready()
|
||||||
|
.and_then(|service| service.call(request))
|
||||||
|
.await
|
||||||
|
.map_err(|error| Error {
|
||||||
|
code: ErrorCode::ServerError(0),
|
||||||
|
message: error.to_string(),
|
||||||
|
data: None,
|
||||||
|
})?;
|
||||||
|
let utxos = match response {
|
||||||
|
zebra_state::ReadResponse::Utxos(utxos) => utxos,
|
||||||
|
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for utxo_data in utxos.utxos() {
|
||||||
|
let address = utxo_data.0.to_string();
|
||||||
|
let txid = utxo_data.1.to_string();
|
||||||
|
let height = utxo_data.2.height().0;
|
||||||
|
let output_index = utxo_data.2.output_index().as_usize();
|
||||||
|
let script = utxo_data.3.lock_script.to_string();
|
||||||
|
let satoshis = i64::from(utxo_data.3.value);
|
||||||
|
|
||||||
|
let entry = GetAddressUtxos {
|
||||||
|
address,
|
||||||
|
txid,
|
||||||
|
height,
|
||||||
|
output_index,
|
||||||
|
script,
|
||||||
|
satoshis,
|
||||||
|
};
|
||||||
|
response_utxos.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response_utxos)
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response to a `getinfo` RPC request.
|
/// Response to a `getinfo` RPC request.
|
||||||
|
|
@ -722,12 +775,37 @@ pub struct GetBlockChainInfo {
|
||||||
|
|
||||||
/// A wrapper type with a list of strings of addresses.
|
/// A wrapper type with a list of strings of addresses.
|
||||||
///
|
///
|
||||||
/// This is used for the input parameter of [`Rpc::get_account_balance`].
|
/// This is used for the input parameter of [`Rpc::get_address_balance`],
|
||||||
|
/// [`Rpc::get_address_tx_ids`] and [`Rpc::get_address_utxos`].
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize)]
|
||||||
pub struct AddressStrings {
|
pub struct AddressStrings {
|
||||||
addresses: Vec<String>,
|
addresses: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AddressStrings {
|
||||||
|
// Creates a new `AddressStrings` given a vector.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn new(addresses: Vec<String>) -> AddressStrings {
|
||||||
|
AddressStrings { addresses }
|
||||||
|
}
|
||||||
|
/// Given a list of addresses as strings:
|
||||||
|
/// - check if provided list have all valid transparent addresses.
|
||||||
|
/// - return valid addresses as a set of `Address`.
|
||||||
|
pub fn valid_addresses(self) -> Result<HashSet<Address>> {
|
||||||
|
let valid_addresses: HashSet<Address> = self
|
||||||
|
.addresses
|
||||||
|
.into_iter()
|
||||||
|
.map(|address| {
|
||||||
|
address.parse().map_err(|error| {
|
||||||
|
Error::invalid_params(&format!("invalid address {address:?}: {error}"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
Ok(valid_addresses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The transparent balance of a set of addresses.
|
/// The transparent balance of a set of addresses.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)]
|
||||||
pub struct AddressBalance {
|
pub struct AddressBalance {
|
||||||
|
|
@ -810,6 +888,20 @@ pub enum GetRawTransaction {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response to a `getaddressutxos` RPC request.
|
||||||
|
///
|
||||||
|
/// See the notes for the [`Rpc::get_address_utxos` method].
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct GetAddressUtxos {
|
||||||
|
address: String,
|
||||||
|
txid: String,
|
||||||
|
height: u32,
|
||||||
|
#[serde(rename = "outputIndex")]
|
||||||
|
output_index: usize,
|
||||||
|
script: String,
|
||||||
|
satoshis: i64,
|
||||||
|
}
|
||||||
|
|
||||||
impl GetRawTransaction {
|
impl GetRawTransaction {
|
||||||
fn from_transaction(
|
fn from_transaction(
|
||||||
tx: Arc<Transaction>,
|
tx: Arc<Transaction>,
|
||||||
|
|
|
||||||
|
|
@ -337,12 +337,15 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
||||||
let start: u32 = 1;
|
let start: u32 = 1;
|
||||||
let end: u32 = 2;
|
let end: u32 = 2;
|
||||||
let error = rpc
|
let error = rpc
|
||||||
.get_address_tx_ids(addresses, start, end)
|
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
error.message,
|
error.message,
|
||||||
format!("Provided address is not valid: {}", address)
|
format!(
|
||||||
|
"invalid address \"{}\": parse error: t-addr decoding error",
|
||||||
|
address
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// create a valid address
|
// create a valid address
|
||||||
|
|
@ -353,7 +356,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
||||||
let start: u32 = 2;
|
let start: u32 = 2;
|
||||||
let end: u32 = 1;
|
let end: u32 = 1;
|
||||||
let error = rpc
|
let error = rpc
|
||||||
.get_address_tx_ids(addresses.clone(), start, end)
|
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -365,7 +368,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
||||||
let start: u32 = 0;
|
let start: u32 = 0;
|
||||||
let end: u32 = 1;
|
let end: u32 = 1;
|
||||||
let error = rpc
|
let error = rpc
|
||||||
.get_address_tx_ids(addresses.clone(), start, end)
|
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -377,7 +380,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
||||||
let start: u32 = 1;
|
let start: u32 = 1;
|
||||||
let end: u32 = 11;
|
let end: u32 = 11;
|
||||||
let error = rpc
|
let error = rpc
|
||||||
.get_address_tx_ids(addresses, start, end)
|
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -456,7 +459,7 @@ async fn rpc_getaddresstxids_response_with(
|
||||||
// call the method with valid arguments
|
// call the method with valid arguments
|
||||||
let addresses = vec![address.to_string()];
|
let addresses = vec![address.to_string()];
|
||||||
let response = rpc
|
let response = rpc
|
||||||
.get_address_tx_ids(addresses, *range.start(), *range.end())
|
.get_address_tx_ids(AddressStrings::new(addresses), *range.start(), *range.end())
|
||||||
.await
|
.await
|
||||||
.expect("arguments are valid so no error can happen here");
|
.expect("arguments are valid so no error can happen here");
|
||||||
|
|
||||||
|
|
@ -482,3 +485,81 @@ async fn rpc_getaddresstxids_response_with(
|
||||||
.is_cancelled()
|
.is_cancelled()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rpc_getaddressutxos_invalid_arguments() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||||
|
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||||
|
|
||||||
|
let rpc = RpcImpl::new(
|
||||||
|
"RPC test",
|
||||||
|
Buffer::new(mempool.clone(), 1),
|
||||||
|
Buffer::new(state.clone(), 1),
|
||||||
|
NoChainTip,
|
||||||
|
Mainnet,
|
||||||
|
);
|
||||||
|
|
||||||
|
// call the method with an invalid address string
|
||||||
|
let address = "11111111".to_string();
|
||||||
|
let addresses = vec![address.clone()];
|
||||||
|
let error = rpc
|
||||||
|
.0
|
||||||
|
.get_address_utxos(AddressStrings::new(addresses))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error.message,
|
||||||
|
format!(
|
||||||
|
"invalid address \"{}\": parse error: t-addr decoding error",
|
||||||
|
address
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
mempool.expect_no_requests().await;
|
||||||
|
state.expect_no_requests().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rpc_getaddressutxos_response() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
let blocks: Vec<Arc<Block>> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS
|
||||||
|
.iter()
|
||||||
|
.map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// get the first transaction of the first block
|
||||||
|
let first_block_first_transaction = &blocks[1].transactions[0];
|
||||||
|
// get the address, this is always `t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd`
|
||||||
|
let address = &first_block_first_transaction.outputs()[1]
|
||||||
|
.address(Mainnet)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||||
|
// Create a populated state service
|
||||||
|
let (_state, read_state, latest_chain_tip, _chain_tip_change) =
|
||||||
|
zebra_state::populated_state(blocks.clone(), Mainnet).await;
|
||||||
|
|
||||||
|
let rpc = RpcImpl::new(
|
||||||
|
"RPC test",
|
||||||
|
Buffer::new(mempool.clone(), 1),
|
||||||
|
Buffer::new(read_state.clone(), 1),
|
||||||
|
latest_chain_tip,
|
||||||
|
Mainnet,
|
||||||
|
);
|
||||||
|
|
||||||
|
// call the method with a valid address
|
||||||
|
let addresses = vec![address.to_string()];
|
||||||
|
let response = rpc
|
||||||
|
.0
|
||||||
|
.get_address_utxos(AddressStrings::new(addresses))
|
||||||
|
.await
|
||||||
|
.expect("address is valid so no error can happen here");
|
||||||
|
|
||||||
|
// there are 10 outputs for provided address
|
||||||
|
assert_eq!(response.len(), 10);
|
||||||
|
|
||||||
|
mempool.expect_no_requests().await;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -462,4 +462,9 @@ pub enum ReadRequest {
|
||||||
/// The blocks to be queried for transactions.
|
/// The blocks to be queried for transactions.
|
||||||
height_range: RangeInclusive<block::Height>,
|
height_range: RangeInclusive<block::Height>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Looks up utxos for the provided addresses.
|
||||||
|
///
|
||||||
|
/// Returns a type with found utxos and transaction information.
|
||||||
|
UtxosByAddresses(HashSet<transparent::Address>),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ use zebra_chain::{
|
||||||
// will work with inline links.
|
// will work with inline links.
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::Request;
|
use crate::Request;
|
||||||
use crate::TransactionLocation;
|
|
||||||
|
use crate::{service::read::AddressUtxos, TransactionLocation};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
/// A response to a [`StateService`] [`Request`].
|
/// A response to a [`StateService`] [`Request`].
|
||||||
|
|
@ -62,4 +63,7 @@ pub enum ReadResponse {
|
||||||
/// Response to [`ReadRequest::TransactionIdsByAddresses`] with the obtained transaction ids,
|
/// Response to [`ReadRequest::TransactionIdsByAddresses`] with the obtained transaction ids,
|
||||||
/// in the order they appear in blocks.
|
/// in the order they appear in blocks.
|
||||||
AddressesTransactionIds(BTreeMap<TransactionLocation, transaction::Hash>),
|
AddressesTransactionIds(BTreeMap<TransactionLocation, transaction::Hash>),
|
||||||
|
|
||||||
|
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
|
||||||
|
Utxos(AddressUtxos),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ pub(crate) mod check;
|
||||||
mod finalized_state;
|
mod finalized_state;
|
||||||
mod non_finalized_state;
|
mod non_finalized_state;
|
||||||
mod pending_utxos;
|
mod pending_utxos;
|
||||||
mod read;
|
pub(crate) mod read;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "proptest-impl"))]
|
#[cfg(any(test, feature = "proptest-impl"))]
|
||||||
pub mod arbitrary;
|
pub mod arbitrary;
|
||||||
|
|
@ -1036,6 +1036,27 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For the get_address_utxos RPC.
|
||||||
|
ReadRequest::UtxosByAddresses(addresses) => {
|
||||||
|
metrics::counter!(
|
||||||
|
"state.requests",
|
||||||
|
1,
|
||||||
|
"service" => "read_state",
|
||||||
|
"type" => "utxos_by_addresses",
|
||||||
|
);
|
||||||
|
|
||||||
|
let state = self.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let utxos = state.best_chain_receiver.with_watch_data(|best_chain| {
|
||||||
|
read::transparent_utxos(state.network, best_chain, &state.db, addresses)
|
||||||
|
});
|
||||||
|
|
||||||
|
utxos.map(ReadResponse::Utxos)
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue