feat(rpc): Implement what we can of `getaddresstxids` RPC method. (#4062)
* implement `getaddresstxids` rpc method with dummy empty response * use already public function * fix some docs * pass a list of addresses to the state request * sync range errors with zcashd * refactor a loop * fix grammar * fix tests Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
43e80fd61c
commit
7b7d22aabc
|
@ -24,6 +24,7 @@ use zebra_chain::{
|
|||
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
||||
serialization::{SerializationError, ZcashDeserialize},
|
||||
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
|
||||
transparent::Address,
|
||||
};
|
||||
use zebra_network::constants::USER_AGENT;
|
||||
use zebra_node_services::{mempool, BoxError};
|
||||
|
@ -144,6 +145,28 @@ pub trait Rpc {
|
|||
txid_hex: String,
|
||||
verbose: u8,
|
||||
) -> BoxFuture<Result<GetRawTransaction>>;
|
||||
|
||||
/// Returns the transaction ids made by the provided transparent addresses.
|
||||
///
|
||||
/// zcashd reference: [`getaddresstxids`](https://zcash.github.io/rpc/getaddresstxids.html)
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `addresses`: (json array of string, required) The addresses to get transactions from.
|
||||
/// - `start`: (numeric, required) The lower height to start looking for transactions (inclusive).
|
||||
/// - `end`: (numeric, required) The top height to stop looking for transactions (inclusive).
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// Only the multi-argument format is used by lightwalletd and this is what we currently support:
|
||||
/// https://github.com/zcash/lightwalletd/blob/631bb16404e3d8b045e74a7c5489db626790b2f6/common/common.go#L97-L102
|
||||
#[rpc(name = "getaddresstxids")]
|
||||
fn get_address_tx_ids(
|
||||
&self,
|
||||
addresses: Vec<String>,
|
||||
start: u32,
|
||||
end: u32,
|
||||
) -> BoxFuture<Result<Vec<String>>>;
|
||||
}
|
||||
|
||||
/// RPC method implementations.
|
||||
|
@ -555,6 +578,59 @@ where
|
|||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn get_address_tx_ids(
|
||||
&self,
|
||||
addresses: Vec<String>,
|
||||
start: u32,
|
||||
end: u32,
|
||||
) -> BoxFuture<Result<Vec<String>>> {
|
||||
let mut state = self.state.clone();
|
||||
let mut response_transactions = vec![];
|
||||
let start = Height(start);
|
||||
let end = Height(end);
|
||||
|
||||
let chain_height = self.latest_chain_tip.best_tip_height().ok_or(Error {
|
||||
code: ErrorCode::ServerError(0),
|
||||
message: "No blocks in state".to_string(),
|
||||
data: None,
|
||||
});
|
||||
|
||||
async move {
|
||||
// height range checks
|
||||
check_height_range(start, end, chain_height?)?;
|
||||
|
||||
let valid_addresses: Result<Vec<Address>> = addresses
|
||||
.iter()
|
||||
.map(|address| {
|
||||
address.parse().map_err(|_| {
|
||||
Error::invalid_params(format!("Provided address is not valid: {}", address))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let request =
|
||||
zebra_state::ReadRequest::TransactionsByAddresses(valid_addresses?, start, end);
|
||||
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,
|
||||
})?;
|
||||
|
||||
match response {
|
||||
zebra_state::ReadResponse::TransactionIds(hashes) => response_transactions
|
||||
.append(&mut hashes.iter().map(|h| h.to_string()).collect()),
|
||||
_ => unreachable!("unmatched response to a TransactionsByAddresses request"),
|
||||
}
|
||||
|
||||
Ok(response_transactions)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// Response to a `getinfo` RPC request.
|
||||
|
@ -679,3 +755,22 @@ impl GetRawTransaction {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if provided height range is valid
|
||||
fn check_height_range(start: Height, end: Height, chain_height: Height) -> Result<()> {
|
||||
if start == Height(0) || end == Height(0) {
|
||||
return Err(Error::invalid_params(
|
||||
"Start and end are expected to be greater than zero",
|
||||
));
|
||||
}
|
||||
if end < start {
|
||||
return Err(Error::invalid_params(
|
||||
"End value is expected to be greater than or equal to start",
|
||||
));
|
||||
}
|
||||
if start > chain_height || end > chain_height {
|
||||
return Err(Error::invalid_params("Start or end is outside chain range"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -305,3 +305,137 @@ async fn rpc_getrawtransaction() {
|
|||
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
|
||||
assert!(matches!(rpc_tx_queue_task_result, None));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rpc_getaddresstxids_invalid_arguments() {
|
||||
zebra_test::init();
|
||||
|
||||
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||
|
||||
// Create a continuous chain of mainnet blocks from genesis
|
||||
let blocks: Vec<Arc<Block>> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS
|
||||
.iter()
|
||||
.map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap())
|
||||
.collect();
|
||||
|
||||
// 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, rpc_tx_queue_task_handle) = RpcImpl::new(
|
||||
"RPC test",
|
||||
Buffer::new(mempool.clone(), 1),
|
||||
Buffer::new(read_state.clone(), 1),
|
||||
latest_chain_tip,
|
||||
Mainnet,
|
||||
);
|
||||
|
||||
// call the method with an invalid address string
|
||||
let address = "11111111".to_string();
|
||||
let addresses = vec![address.clone()];
|
||||
let start: u32 = 1;
|
||||
let end: u32 = 2;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses, start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.message,
|
||||
format!("Provided address is not valid: {}", address)
|
||||
);
|
||||
|
||||
// create a valid address
|
||||
let address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string();
|
||||
let addresses = vec![address.clone()];
|
||||
|
||||
// call the method with start greater than end
|
||||
let start: u32 = 2;
|
||||
let end: u32 = 1;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses.clone(), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"End value is expected to be greater than or equal to start".to_string()
|
||||
);
|
||||
|
||||
// call the method with start equal zero
|
||||
let start: u32 = 0;
|
||||
let end: u32 = 1;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses.clone(), start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"Start and end are expected to be greater than zero".to_string()
|
||||
);
|
||||
|
||||
// call the method outside the chain tip height
|
||||
let start: u32 = 1;
|
||||
let end: u32 = 11;
|
||||
let error = rpc
|
||||
.get_address_tx_ids(addresses, start, end)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.message,
|
||||
"Start or end is outside chain range".to_string()
|
||||
);
|
||||
|
||||
mempool.expect_no_requests().await;
|
||||
|
||||
// The queue task should continue without errors or panics
|
||||
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
|
||||
assert!(matches!(rpc_tx_queue_task_result, None));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rpc_getaddresstxids_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, rpc_tx_queue_task_handle) = RpcImpl::new(
|
||||
"RPC test",
|
||||
Buffer::new(mempool.clone(), 1),
|
||||
Buffer::new(read_state.clone(), 1),
|
||||
latest_chain_tip,
|
||||
Mainnet,
|
||||
);
|
||||
|
||||
// call the method with valid arguments
|
||||
let addresses = vec![address.to_string()];
|
||||
let start: u32 = 1;
|
||||
let end: u32 = 1;
|
||||
let response = rpc
|
||||
.get_address_tx_ids(addresses, start, end)
|
||||
.await
|
||||
.expect("arguments are valid so no error can happen here");
|
||||
|
||||
// TODO: The lenght of the response should be 1
|
||||
// Fix in the context of #3147
|
||||
assert_eq!(response.len(), 0);
|
||||
|
||||
mempool.expect_no_requests().await;
|
||||
|
||||
// The queue task should continue without errors or panics
|
||||
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
|
||||
assert!(matches!(rpc_tx_queue_task_result, None));
|
||||
}
|
||||
|
|
|
@ -413,7 +413,7 @@ pub enum Request {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// A read-only query about the chain state, via the [`ReadStateService`].
|
||||
pub enum ReadRequest {
|
||||
/// Looks up a block by hash or height in the current best chain.
|
||||
|
@ -434,4 +434,15 @@ pub enum ReadRequest {
|
|||
/// * [`Response::Transaction(Some(Arc<Transaction>))`](Response::Transaction) if the transaction is in the best chain;
|
||||
/// * [`Response::Transaction(None)`](Response::Transaction) otherwise.
|
||||
Transaction(transaction::Hash),
|
||||
|
||||
/// Looks up transactions hashes that were made by provided addresses in a blockchain height range.
|
||||
///
|
||||
/// Returns
|
||||
///
|
||||
/// * A vector of transaction hashes.
|
||||
/// * An empty vector if no transactions were found for the given arguments.
|
||||
///
|
||||
/// Returned txids are in the order they appear in blocks, which ensures that they are topologically sorted
|
||||
/// (i.e. parent txids will appear before child txids).
|
||||
TransactionsByAddresses(Vec<transparent::Address>, block::Height, block::Height),
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
|||
|
||||
use zebra_chain::{
|
||||
block::{self, Block},
|
||||
transaction::Transaction,
|
||||
transaction::{Hash, Transaction},
|
||||
transparent,
|
||||
};
|
||||
|
||||
|
@ -53,4 +53,8 @@ pub enum ReadResponse {
|
|||
|
||||
/// Response to [`ReadRequest::Transaction`] with the specified transaction.
|
||||
Transaction(Option<(Arc<Transaction>, block::Height)>),
|
||||
|
||||
/// Response to [`ReadRequest::TransactionsByAddresses`] with the obtained transaction ids,
|
||||
/// in the order they appear in blocks.
|
||||
TransactionIds(Vec<Hash>),
|
||||
}
|
||||
|
|
|
@ -970,7 +970,7 @@ impl Service<ReadRequest> for ReadStateService {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
// For the get_raw_transaction RPC, to be implemented in #3145.
|
||||
// For the get_raw_transaction RPC.
|
||||
ReadRequest::Transaction(hash) => {
|
||||
metrics::counter!(
|
||||
"state.requests",
|
||||
|
@ -991,6 +991,29 @@ impl Service<ReadRequest> for ReadStateService {
|
|||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
// For the get_address_tx_ids RPC.
|
||||
ReadRequest::TransactionsByAddresses(_addresses, _start, _end) => {
|
||||
metrics::counter!(
|
||||
"state.requests",
|
||||
1,
|
||||
"service" => "read_state",
|
||||
"type" => "transactions_by_addresses",
|
||||
);
|
||||
|
||||
let _state = self.clone();
|
||||
|
||||
async move {
|
||||
// TODO: Respond with found transactions
|
||||
// At least the following pull requests should be merged:
|
||||
// - #4022
|
||||
// - #4038
|
||||
// Do the corresponding update in the context of #3147
|
||||
let transaction_ids = vec![];
|
||||
Ok(ReadResponse::TransactionIds(transaction_ids))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue