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 hex::ToHex;
|
||||
|
||||
use crate::serialization::{
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
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 {
|
||||
fn zcash_serialize<W: io::Write>(&self, writer: W) -> Result<(), io::Error> {
|
||||
zcash_serialize_bytes(&self.0, writer)
|
||||
|
|
|
@ -190,10 +190,28 @@ pub trait Rpc {
|
|||
#[rpc(name = "getaddresstxids")]
|
||||
fn get_address_tx_ids(
|
||||
&self,
|
||||
addresses: Vec<String>,
|
||||
address_strings: AddressStrings,
|
||||
start: u32,
|
||||
end: u32,
|
||||
) -> 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.
|
||||
|
@ -403,17 +421,9 @@ where
|
|||
let state = self.state.clone();
|
||||
|
||||
async move {
|
||||
let addresses: HashSet<Address> = address_strings
|
||||
.addresses
|
||||
.into_iter()
|
||||
.map(|address| {
|
||||
address.parse().map_err(|error| {
|
||||
Error::invalid_params(&format!("invalid address {address:?}: {error}"))
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
let valid_addresses = address_strings.valid_addresses()?;
|
||||
|
||||
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 {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: error.to_string(),
|
||||
|
@ -642,7 +652,7 @@ where
|
|||
|
||||
fn get_address_tx_ids(
|
||||
&self,
|
||||
addresses: Vec<String>,
|
||||
address_strings: AddressStrings,
|
||||
start: u32,
|
||||
end: u32,
|
||||
) -> BoxFuture<Result<Vec<String>>> {
|
||||
|
@ -660,17 +670,10 @@ where
|
|||
// height range checks
|
||||
check_height_range(start, end, chain_height?)?;
|
||||
|
||||
let valid_addresses: Result<HashSet<Address>> = addresses
|
||||
.iter()
|
||||
.map(|address| {
|
||||
address.parse().map_err(|_| {
|
||||
Error::invalid_params(format!("Provided address is not valid: {}", address))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let valid_addresses = address_strings.valid_addresses()?;
|
||||
|
||||
let request = zebra_state::ReadRequest::TransactionIdsByAddresses {
|
||||
addresses: valid_addresses?,
|
||||
addresses: valid_addresses,
|
||||
height_range: start..=end,
|
||||
};
|
||||
let response = state
|
||||
|
@ -694,6 +697,56 @@ where
|
|||
}
|
||||
.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.
|
||||
|
@ -722,12 +775,37 @@ pub struct GetBlockChainInfo {
|
|||
|
||||
/// 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)]
|
||||
pub struct AddressStrings {
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, serde::Serialize)]
|
||||
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 {
|
||||
fn from_transaction(
|
||||
tx: Arc<Transaction>,
|
||||
|
|
|
@ -337,12 +337,15 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
|||
let start: u32 = 1;
|
||||
let end: u32 = 2;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses, start, end)
|
||||
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.message,
|
||||
format!("Provided address is not valid: {}", address)
|
||||
format!(
|
||||
"invalid address \"{}\": parse error: t-addr decoding error",
|
||||
address
|
||||
)
|
||||
);
|
||||
|
||||
// create a valid address
|
||||
|
@ -353,7 +356,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
|||
let start: u32 = 2;
|
||||
let end: u32 = 1;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses.clone(), start, end)
|
||||
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
|
@ -365,7 +368,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
|||
let start: u32 = 0;
|
||||
let end: u32 = 1;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses.clone(), start, end)
|
||||
.get_address_tx_ids(AddressStrings::new(addresses.clone()), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
|
@ -377,7 +380,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
|
|||
let start: u32 = 1;
|
||||
let end: u32 = 11;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses, start, end)
|
||||
.get_address_tx_ids(AddressStrings::new(addresses), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
|
@ -456,7 +459,7 @@ async fn rpc_getaddresstxids_response_with(
|
|||
// call the method with valid arguments
|
||||
let addresses = vec![address.to_string()];
|
||||
let response = rpc
|
||||
.get_address_tx_ids(addresses, *range.start(), *range.end())
|
||||
.get_address_tx_ids(AddressStrings::new(addresses), *range.start(), *range.end())
|
||||
.await
|
||||
.expect("arguments are valid so no error can happen here");
|
||||
|
||||
|
@ -482,3 +485,81 @@ async fn rpc_getaddresstxids_response_with(
|
|||
.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.
|
||||
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.
|
||||
#[allow(unused_imports)]
|
||||
use crate::Request;
|
||||
use crate::TransactionLocation;
|
||||
|
||||
use crate::{service::read::AddressUtxos, TransactionLocation};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// A response to a [`StateService`] [`Request`].
|
||||
|
@ -62,4 +63,7 @@ pub enum ReadResponse {
|
|||
/// Response to [`ReadRequest::TransactionIdsByAddresses`] with the obtained transaction ids,
|
||||
/// in the order they appear in blocks.
|
||||
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 non_finalized_state;
|
||||
mod pending_utxos;
|
||||
mod read;
|
||||
pub(crate) mod read;
|
||||
|
||||
#[cfg(any(test, feature = "proptest-impl"))]
|
||||
pub mod arbitrary;
|
||||
|
@ -1036,6 +1036,27 @@ impl Service<ReadRequest> for ReadStateService {
|
|||
}
|
||||
.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