use std::{
cmp,
collections::{HashMap, HashSet},
fmt,
ops::RangeInclusive,
sync::Arc,
time::Duration,
};
use chrono::Utc;
use derive_getters::Getters;
use derive_new::new;
use futures::{future::OptionFuture, stream::FuturesOrdered, StreamExt, TryFutureExt};
use hex::{FromHex, ToHex};
use indexmap::IndexMap;
use jsonrpsee::core::{async_trait, RpcResult as Result};
use jsonrpsee_proc_macros::rpc;
use jsonrpsee_types::{ErrorCode, ErrorObject};
use schemars::JsonSchema;
use serde::Deserialize;
use tokio::{
sync::{broadcast, mpsc, watch},
task::JoinHandle,
};
use tower::ServiceExt;
use tracing::Instrument;
use zakura_chain::{
amount::{Amount, NegativeAllowed},
block::{self, Block, Commitment, Height, SerializedBlock, TryIntoHeight},
chain_sync_status::ChainSyncStatus,
chain_tip::{ChainTip, NetworkChainTipHeightEstimator},
parameters::{
subsidy::{
block_subsidy, founders_reward, funding_stream_values, miner_subsidy,
FundingStreamReceiver,
},
ConsensusBranchId, Network, NetworkUpgrade, POW_AVERAGING_WINDOW,
},
serialization::{BytesInDisplayOrder, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
subtree::NoteCommitmentSubtreeIndex,
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, Address, OutputIndex},
value_balance::ValueBalance,
work::{
difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty, U256},
equihash::Solution,
},
};
use zakura_consensus::{
funding_stream_address, router::service_trait::BlockVerifierService, RouterError,
};
use zakura_network::{address_book_peers::AddressBookPeers, types::PeerServices, PeerSocketAddr};
use zakura_node_services::mempool::{self, CreatedOrSpent, MempoolService};
use zakura_state::{
AnyTx, HashOrHeight, OutputLocation, ReadRequest, ReadResponse, ReadState as ReadStateService,
State as StateService, TransactionLocation,
};
use zcash_address::{unified::Encoding, TryFromAddress};
use zcash_protocol::consensus::{self, Parameters};
use crate::{
client::TransactionTemplate,
client::Treestate,
config,
methods::types::{
validate_address::validate_address, z_validate_address::z_validate_address, zec::Zec,
},
queue::Queue,
server::{
self,
error::{MapError, OkOrError},
},
};
pub(crate) mod hex_data;
pub(crate) mod trees;
pub(crate) mod types;
use hex_data::HexData;
use trees::{GetSubtreesByIndexResponse, GetTreestateResponse, SubtreeRpcData};
use types::{
get_block_template::{
constants::{
DEFAULT_SOLUTION_RATE_WINDOW_SIZE, MEMPOOL_LONG_POLL_INTERVAL,
ZCASHD_FUNDING_STREAM_ORDER,
},
proposal::proposal_block_from_template,
BlockTemplateResponse, BlockTemplateTimeSource, GetBlockTemplateHandler,
GetBlockTemplateParameters, GetBlockTemplateResponse,
},
get_blockchain_info::GetBlockchainInfoBalance,
get_mempool_info::GetMempoolInfoResponse,
get_mining_info::GetMiningInfoResponse,
get_raw_mempool::{self, GetRawMempoolResponse},
long_poll::LongPollInput,
network_info::{GetNetworkInfoResponse, NetworkInfo},
peer_info::PeerInfo,
submit_block::{SubmitBlockErrorResponse, SubmitBlockParameters, SubmitBlockResponse},
subsidy::GetBlockSubsidyResponse,
transaction::TransactionObject,
unified_address::ZListUnifiedReceiversResponse,
validate_address::ValidateAddressResponse,
z_validate_address::ZValidateAddressResponse,
};
include!(concat!(env!("OUT_DIR"), "/rpc_openrpc.rs"));
pub(super) const PARAM_VERBOSE_DESC: &str =
"Boolean flag to indicate verbosity, true for a json object, false for hex encoded data.";
pub(super) const PARAM_POOL_DESC: &str = "The pool from which subtrees should be returned. \
Either \"sapling\", \"orchard\", or \"ironwood\".";
pub(super) const PARAM_START_INDEX_DESC: &str =
"The index of the first 2^16-leaf subtree to return.";
pub(super) const PARAM_LIMIT_DESC: &str = "The maximum number of subtrees to return.";
pub(super) const PARAM_REQUEST_DESC: &str = "The request object containing the parameters.";
pub(super) const PARAM_INDEX_DESC: &str = "The index of the subtree to return.";
pub(super) const PARAM_RAW_TRANSACTION_HEX_DESC: &str = "The hex-encoded raw transaction bytes.";
#[allow(non_upper_case_globals)]
pub(super) const PARAM__ALLOW_HIGH_FEES_DESC: &str = "Whether to allow high fees.";
pub(super) const PARAM_NUM_BLOCKS_DESC: &str = "The number of blocks to return.";
pub(super) const PARAM_HEIGHT_DESC: &str = "The height of the block to return.";
pub(super) const PARAM_COMMAND_DESC: &str = "The command to execute.";
#[allow(non_upper_case_globals)]
pub(super) const PARAM__PARAMETERS_DESC: &str = "The parameters for the command.";
pub(super) const PARAM_BLOCK_HASH_DESC: &str = "The hash of the block to return.";
pub(super) const PARAM_ADDRESS_DESC: &str = "The address to return.";
pub(super) const PARAM_ADDRESS_STRINGS_DESC: &str = "The addresses to return.";
pub(super) const PARAM_ADDR_DESC: &str = "The address to return.";
pub(super) const PARAM_HEX_DATA_DESC: &str = "The hex-encoded data to return.";
pub(super) const PARAM_TXID_DESC: &str = "The transaction ID to return.";
pub(super) const PARAM_HASH_OR_HEIGHT_DESC: &str = "The block hash or height to return.";
pub(super) const PARAM_PARAMETERS_DESC: &str = "The parameters for the command.";
pub(super) const PARAM_VERBOSITY_DESC: &str = "Whether to include verbose output.";
pub(super) const PARAM_N_DESC: &str = "The output index in the transaction.";
pub(super) const PARAM_INCLUDE_MEMPOOL_DESC: &str =
"Whether to include mempool transactions in the response.";
#[cfg(test)]
mod tests;
#[rpc(server)]
pub trait Rpc {
#[method(name = "getinfo")]
async fn get_info(&self) -> Result<GetInfoResponse>;
#[method(name = "getblockchaininfo")]
async fn get_blockchain_info(&self) -> Result<GetBlockchainInfoResponse>;
#[method(name = "getaddressbalance")]
async fn get_address_balance(
&self,
address_strings: GetAddressBalanceRequest,
) -> Result<GetAddressBalanceResponse>;
#[method(name = "sendrawtransaction")]
async fn send_raw_transaction(
&self,
raw_transaction_hex: String,
_allow_high_fees: Option<bool>,
) -> Result<SendRawTransactionResponse>;
#[method(name = "getblock")]
async fn get_block(
&self,
hash_or_height: String,
verbosity: Option<u8>,
) -> Result<GetBlockResponse>;
#[method(name = "getblockheader")]
async fn get_block_header(
&self,
hash_or_height: String,
verbose: Option<bool>,
) -> Result<GetBlockHeaderResponse>;
#[method(name = "getbestblockhash")]
fn get_best_block_hash(&self) -> Result<GetBlockHashResponse>;
#[method(name = "getbestblockheightandhash")]
fn get_best_block_height_and_hash(&self) -> Result<GetBlockHeightAndHashResponse>;
#[method(name = "getmempoolinfo")]
async fn get_mempool_info(&self) -> Result<GetMempoolInfoResponse>;
#[method(name = "getrawmempool")]
async fn get_raw_mempool(&self, verbose: Option<bool>) -> Result<GetRawMempoolResponse>;
#[method(name = "z_gettreestate")]
async fn z_get_treestate(&self, hash_or_height: String) -> Result<GetTreestateResponse>;
#[method(name = "z_getsubtreesbyindex")]
async fn z_get_subtrees_by_index(
&self,
pool: String,
start_index: NoteCommitmentSubtreeIndex,
limit: Option<NoteCommitmentSubtreeIndex>,
) -> Result<GetSubtreesByIndexResponse>;
#[method(name = "getrawtransaction")]
async fn get_raw_transaction(
&self,
txid: String,
verbose: Option<u8>,
block_hash: Option<String>,
) -> Result<GetRawTransactionResponse>;
#[method(name = "getaddresstxids")]
async fn get_address_tx_ids(&self, request: GetAddressTxIdsRequest) -> Result<Vec<String>>;
#[method(name = "getaddressutxos")]
async fn get_address_utxos(
&self,
request: GetAddressUtxosRequest,
) -> Result<GetAddressUtxosResponse>;
#[method(name = "stop")]
fn stop(&self) -> Result<String>;
#[method(name = "getblockcount")]
fn get_block_count(&self) -> Result<u32>;
#[method(name = "getblockhash")]
async fn get_block_hash(&self, index: i32) -> Result<GetBlockHashResponse>;
#[method(name = "getblocktemplate")]
async fn get_block_template(
&self,
parameters: Option<GetBlockTemplateParameters>,
) -> Result<GetBlockTemplateResponse>;
#[method(name = "submitblock")]
async fn submit_block(
&self,
hex_data: HexData,
_parameters: Option<SubmitBlockParameters>,
) -> Result<SubmitBlockResponse>;
#[method(name = "getmininginfo")]
async fn get_mining_info(&self) -> Result<GetMiningInfoResponse>;
#[method(name = "getnetworksolps")]
async fn get_network_sol_ps(&self, num_blocks: Option<i32>, height: Option<i32>)
-> Result<u64>;
#[method(name = "getnetworkhashps")]
async fn get_network_hash_ps(
&self,
num_blocks: Option<i32>,
height: Option<i32>,
) -> Result<u64> {
self.get_network_sol_ps(num_blocks, height).await
}
#[method(name = "getnetworkinfo")]
async fn get_network_info(&self) -> Result<GetNetworkInfoResponse>;
#[method(name = "getpeerinfo")]
async fn get_peer_info(&self) -> Result<Vec<PeerInfo>>;
#[method(name = "ping")]
async fn ping(&self) -> Result<()>;
#[method(name = "validateaddress")]
async fn validate_address(&self, address: String) -> Result<ValidateAddressResponse>;
#[method(name = "z_validateaddress")]
async fn z_validate_address(&self, address: String) -> Result<ZValidateAddressResponse>;
#[method(name = "getblocksubsidy")]
async fn get_block_subsidy(&self, height: Option<u32>) -> Result<GetBlockSubsidyResponse>;
#[method(name = "getdifficulty")]
async fn get_difficulty(&self) -> Result<f64>;
#[method(name = "z_listunifiedreceivers")]
async fn z_list_unified_receivers(
&self,
address: String,
) -> Result<ZListUnifiedReceiversResponse>;
#[method(name = "invalidateblock")]
async fn invalidate_block(&self, block_hash: String) -> Result<()>;
#[method(name = "reconsiderblock")]
async fn reconsider_block(&self, block_hash: String) -> Result<Vec<block::Hash>>;
#[method(name = "generate")]
async fn generate(&self, num_blocks: u32) -> Result<Vec<GetBlockHashResponse>>;
#[method(name = "addnode")]
async fn add_node(&self, addr: PeerSocketAddr, command: AddNodeCommand) -> Result<()>;
#[method(name = "rpc.discover")]
fn openrpc(&self) -> openrpsee::openrpc::Response;
#[method(name = "gettxout")]
async fn get_tx_out(
&self,
txid: String,
n: u32,
include_mempool: Option<bool>,
) -> Result<GetTxOutResponse>;
}
#[derive(Clone)]
pub struct RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>
where
Mempool: MempoolService,
State: StateService,
ReadState: ReadStateService,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
BlockVerifierRouter: BlockVerifierService,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
{
build_version: String,
user_agent: String,
network: Network,
debug_force_finished_sync: bool,
mempool: Mempool,
state: State,
read_state: ReadState,
latest_chain_tip: Tip,
queue_sender: broadcast::Sender<UnminedTx>,
address_book: AddressBook,
last_warn_error_log_rx: LoggedLastEvent,
gbt: GetBlockTemplateHandler<BlockVerifierRouter, SyncStatus>,
}
pub type LoggedLastEvent = watch::Receiver<Option<(String, tracing::Level, chrono::DateTime<Utc>)>>;
impl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus> fmt::Debug
for RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>
where
Mempool: MempoolService,
State: StateService,
ReadState: ReadStateService,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
BlockVerifierRouter: BlockVerifierService,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RpcImpl")
.field("build_version", &self.build_version)
.field("user_agent", &self.user_agent)
.field("network", &self.network)
.field("debug_force_finished_sync", &self.debug_force_finished_sync)
.field("getblocktemplate", &self.gbt)
.finish()
}
}
impl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>
RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>
where
Mempool: MempoolService,
State: StateService,
ReadState: ReadStateService,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
BlockVerifierRouter: BlockVerifierService,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
{
#[allow(clippy::too_many_arguments)]
pub fn new<VersionString, UserAgentString>(
network: Network,
mining_config: config::mining::Config,
debug_force_finished_sync: bool,
build_version: VersionString,
user_agent: UserAgentString,
mempool: Mempool,
state: State,
read_state: ReadState,
block_verifier_router: BlockVerifierRouter,
sync_status: SyncStatus,
latest_chain_tip: Tip,
address_book: AddressBook,
last_warn_error_log_rx: LoggedLastEvent,
mined_block_sender: Option<mpsc::Sender<(block::Hash, block::Height)>>,
) -> (Self, JoinHandle<()>)
where
VersionString: ToString + Clone + Send + 'static,
UserAgentString: ToString + Clone + Send + 'static,
{
let (runner, queue_sender) = Queue::start();
let mut build_version = build_version.to_string();
let user_agent = user_agent.to_string();
if !build_version.is_empty() && !build_version.starts_with('v') {
build_version.insert(0, 'v');
}
let gbt = GetBlockTemplateHandler::new(
&network,
mining_config.clone(),
block_verifier_router,
sync_status,
mined_block_sender,
);
let rpc_impl = RpcImpl {
build_version,
user_agent,
network: network.clone(),
debug_force_finished_sync,
mempool: mempool.clone(),
state: state.clone(),
read_state: read_state.clone(),
latest_chain_tip: latest_chain_tip.clone(),
queue_sender,
address_book,
last_warn_error_log_rx,
gbt,
};
let rpc_tx_queue_task_handle = tokio::spawn(
runner
.run(mempool, read_state, latest_chain_tip, network)
.in_current_span(),
);
(rpc_impl, rpc_tx_queue_task_handle)
}
pub fn network(&self) -> &Network {
&self.network
}
}
#[async_trait]
impl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus> RpcServer
for RpcImpl<Mempool, State, ReadState, Tip, AddressBook, BlockVerifierRouter, SyncStatus>
where
Mempool: MempoolService,
State: StateService,
ReadState: ReadStateService,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
BlockVerifierRouter: BlockVerifierService,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
{
async fn get_info(&self) -> Result<GetInfoResponse> {
let version = GetInfoResponse::version_from_string(&self.build_version)
.expect("invalid version string");
let connections = self.address_book.recently_live_peers(Utc::now()).len();
let last_error_recorded = self.last_warn_error_log_rx.borrow().clone();
let (last_error_log, _level, last_error_log_time) = last_error_recorded.unwrap_or((
GetInfoResponse::default().errors,
tracing::Level::INFO,
Utc::now(),
));
let tip_height = self
.latest_chain_tip
.best_tip_height()
.unwrap_or(Height::MIN);
let testnet = self.network.is_a_test_network();
let pay_tx_fee = 0.0;
let relay_fee = zakura_chain::transaction::zip317::MIN_MEMPOOL_TX_FEE_RATE as f64
/ (zakura_chain::amount::COIN as f64);
let difficulty = chain_tip_difficulty(self.network.clone(), self.read_state.clone(), true)
.await
.expect("should always be Ok when `should_use_default` is true");
let response = GetInfoResponse {
version,
build: self.build_version.clone(),
subversion: self.user_agent.clone(),
protocol_version: zakura_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION.0,
blocks: tip_height.0,
connections,
proxy: None,
difficulty,
testnet,
pay_tx_fee,
relay_fee,
errors: last_error_log,
errors_timestamp: last_error_log_time.timestamp(),
};
Ok(response)
}
#[allow(clippy::unwrap_in_result)]
async fn get_blockchain_info(&self) -> Result<GetBlockchainInfoResponse> {
let debug_force_finished_sync = self.debug_force_finished_sync;
let network = &self.network;
let (usage_info_rsp, is_pruned_rsp, tip_pool_values_rsp, chain_tip_difficulty) = {
use zakura_state::ReadRequest::*;
let state_call = |request| self.read_state.clone().oneshot(request);
tokio::join!(
state_call(UsageInfo),
state_call(IsPruned),
state_call(TipPoolValues),
chain_tip_difficulty(network.clone(), self.read_state.clone(), true)
)
};
let (size_on_disk, is_pruned, (tip_height, tip_hash), value_balance, difficulty) = {
use zakura_state::ReadResponse::*;
let UsageInfo(size_on_disk) = usage_info_rsp.map_misc_error()? else {
unreachable!("unmatched response to a UsageInfo request")
};
let IsPruned(is_pruned) = is_pruned_rsp.map_misc_error()? else {
unreachable!("unmatched response to an IsPruned request")
};
let (tip, value_balance) = match tip_pool_values_rsp {
Ok(TipPoolValues {
tip_height,
tip_hash,
value_balance,
}) => ((tip_height, tip_hash), value_balance),
Ok(_) => unreachable!("unmatched response to a TipPoolValues request"),
Err(_) => ((Height::MIN, network.genesis_hash()), Default::default()),
};
let difficulty = chain_tip_difficulty
.expect("should always be Ok when `should_use_default` is true");
(size_on_disk, is_pruned, tip, value_balance, difficulty)
};
let now = Utc::now();
let (estimated_height, verification_progress) = self
.latest_chain_tip
.best_tip_height_and_block_time()
.map(|(tip_height, tip_block_time)| {
let height =
NetworkChainTipHeightEstimator::new(tip_block_time, tip_height, network)
.estimate_height_at(now);
let height =
if tip_block_time > now || height < tip_height || debug_force_finished_sync {
tip_height
} else {
height
};
(height, f64::from(tip_height.0) / f64::from(height.0))
})
.unwrap_or((Height::MIN, 0.0));
let verification_progress = if network.is_regtest() {
1.0
} else {
verification_progress
};
let mut upgrades = IndexMap::new();
for (activation_height, network_upgrade) in network.full_activation_list() {
if let Some(branch_id) = network_upgrade.branch_id() {
let status = if tip_height >= activation_height {
NetworkUpgradeStatus::Active
} else {
NetworkUpgradeStatus::Pending
};
let upgrade = NetworkUpgradeInfo {
name: network_upgrade,
activation_height,
status,
};
upgrades.insert(ConsensusBranchIdHex(branch_id), upgrade);
}
}
let next_block_height =
(tip_height + 1).expect("valid chain tips are a lot less than Height::MAX");
let consensus = TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(
NetworkUpgrade::current(network, tip_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
next_block: ConsensusBranchIdHex(
NetworkUpgrade::current(network, next_block_height)
.branch_id()
.unwrap_or(ConsensusBranchId::RPC_MISSING_ID),
),
};
let response = GetBlockchainInfoResponse {
chain: network.bip70_network_name(),
blocks: tip_height,
best_block_hash: tip_hash,
estimated_height,
chain_supply: GetBlockchainInfoBalance::chain_supply(value_balance),
value_pools: GetBlockchainInfoBalance::value_pools(value_balance, None),
upgrades,
consensus,
headers: tip_height,
difficulty,
verification_progress,
chain_work: 0,
pruned: is_pruned,
size_on_disk,
commitments: 0,
};
Ok(response)
}
async fn get_address_balance(
&self,
address_strings: GetAddressBalanceRequest,
) -> Result<GetAddressBalanceResponse> {
let valid_addresses = address_strings.valid_addresses()?;
let request = zakura_state::ReadRequest::AddressBalance(valid_addresses);
let response = self
.read_state
.clone()
.oneshot(request)
.await
.map_misc_error()?;
match response {
zakura_state::ReadResponse::AddressBalance { balance, received } => {
Ok(GetAddressBalanceResponse {
balance: u64::from(balance),
received,
})
}
_ => unreachable!("Unexpected response from state service: {response:?}"),
}
}
async fn send_raw_transaction(
&self,
raw_transaction_hex: String,
_allow_high_fees: Option<bool>,
) -> Result<SendRawTransactionResponse> {
let mempool = self.mempool.clone();
let queue_sender = self.queue_sender.clone();
let raw_transaction_bytes = Vec::from_hex(raw_transaction_hex)
.map_error(server::error::LegacyCode::Deserialization)?;
let raw_transaction = Transaction::zcash_deserialize(&*raw_transaction_bytes)
.map_error(server::error::LegacyCode::Deserialization)?;
let transaction_hash = raw_transaction.hash();
let unmined_transaction = UnminedTx::from(raw_transaction.clone());
let _ = queue_sender.send(unmined_transaction);
let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into());
let request = mempool::Request::Queue(vec![transaction_parameter]);
let response = mempool.oneshot(request).await.map_misc_error()?;
let mut queue_results = match response {
mempool::Response::Queued(results) => results,
_ => unreachable!("incorrect response variant from mempool service"),
};
assert_eq!(
queue_results.len(),
1,
"mempool service returned more results than expected"
);
let queue_result = queue_results
.pop()
.expect("there should be exactly one item in Vec")
.inspect_err(|err| tracing::debug!("sent transaction to mempool: {:?}", &err))
.map_misc_error()?
.await
.map_misc_error()?;
tracing::debug!("sent transaction to mempool: {:?}", &queue_result);
queue_result
.map(|_| SendRawTransactionResponse(transaction_hash))
.map_error(server::error::LegacyCode::Verify)
}
async fn get_block(
&self,
hash_or_height: String,
verbosity: Option<u8>,
) -> Result<GetBlockResponse> {
let verbosity = verbosity.unwrap_or(1);
let network = self.network.clone();
let original_hash_or_height = hash_or_height.clone();
let get_block_header_future = if matches!(verbosity, 1 | 2) {
Some(self.get_block_header(original_hash_or_height.clone(), Some(true)))
} else {
None
};
let hash_or_height =
HashOrHeight::new(&hash_or_height, self.latest_chain_tip.best_tip_height())
.map_error(server::error::LegacyCode::InvalidParameter)?;
if verbosity == 0 {
let request = zakura_state::ReadRequest::Block(hash_or_height);
let response = self
.read_state
.clone()
.oneshot(request)
.await
.map_misc_error()?;
match response {
zakura_state::ReadResponse::Block(Some(block)) => {
Ok(GetBlockResponse::Raw(block.into()))
}
zakura_state::ReadResponse::Block(None) => {
Err("Block not found").map_error(server::error::LegacyCode::InvalidParameter)
}
_ => unreachable!("unmatched response to a block request"),
}
} else if let Some(get_block_header_future) = get_block_header_future {
let get_block_header_result: Result<GetBlockHeaderResponse> =
get_block_header_future.await;
let GetBlockHeaderResponse::Object(block_header) = get_block_header_result? else {
panic!("must return Object")
};
let BlockHeaderObject {
hash,
confirmations,
height,
version,
merkle_root,
block_commitments,
final_sapling_root,
sapling_tree_size,
time,
nonce,
solution,
bits,
difficulty,
previous_block_hash,
next_block_hash,
} = *block_header;
let hash_or_height = hash.into();
let transactions_request = match verbosity {
1 => zakura_state::ReadRequest::TransactionIdsForBlock(hash_or_height),
2 => zakura_state::ReadRequest::BlockAndSize(hash_or_height),
_other => panic!("get_block_header_fut should be none"),
};
let mut requests = vec![
transactions_request,
zakura_state::ReadRequest::OrchardTree(hash_or_height),
];
let nu6_3_active =
network.is_nu_active(consensus::NetworkUpgrade::Nu6_3, height.into());
if nu6_3_active {
requests.push(zakura_state::ReadRequest::IronwoodTree(hash_or_height));
}
requests.extend([
zakura_state::ReadRequest::BlockInfo(previous_block_hash.into()),
zakura_state::ReadRequest::BlockInfo(hash_or_height),
]);
let mut futs = FuturesOrdered::new();
for request in requests {
futs.push_back(self.read_state.clone().oneshot(request));
}
let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
let (tx, size): (Vec<_>, Option<usize>) = match tx_ids_response.map_misc_error()? {
zakura_state::ReadResponse::TransactionIdsForBlock(tx_ids) => (
tx_ids
.ok_or_misc_error("block not found")?
.iter()
.map(|tx_id| GetBlockTransaction::Hash(*tx_id))
.collect(),
None,
),
zakura_state::ReadResponse::BlockAndSize(block_and_size) => {
let (block, size) = block_and_size.ok_or_misc_error("Block not found")?;
let block_time = block.header.time;
let transactions = block
.transactions
.iter()
.map(|tx| {
GetBlockTransaction::Object(Box::new(
TransactionObject::from_transaction(
tx.clone(),
Some(height),
Some(confirmations),
&network,
Some(block_time),
Some(hash),
Some(true),
tx.hash(),
),
))
})
.collect();
(transactions, Some(size))
}
_ => unreachable!("unmatched response to a transaction_ids_for_block request"),
};
let orchard_tree_response = futs.next().await.expect("`futs` should not be empty");
let zakura_state::ReadResponse::OrchardTree(orchard_tree) =
orchard_tree_response.map_misc_error()?
else {
unreachable!("unmatched response to a OrchardTree request");
};
let nu5_activation = NetworkUpgrade::Nu5.activation_height(&network);
let orchard_tree = orchard_tree.ok_or_misc_error("missing Orchard tree")?;
let final_orchard_root = match nu5_activation {
Some(activation_height) if height >= activation_height => {
Some(orchard_tree.root().into())
}
_other => None,
};
let sapling = SaplingTrees {
size: sapling_tree_size,
};
let orchard_tree_size = orchard_tree.count();
let orchard = OrchardTrees {
size: orchard_tree_size,
};
let ironwood = if nu6_3_active {
let ironwood_tree_response = futs.next().await.expect("`futs` should not be empty");
let zakura_state::ReadResponse::IronwoodTree(ironwood_tree) =
ironwood_tree_response.map_misc_error()?
else {
unreachable!("unmatched response to an IronwoodTree request");
};
let ironwood_tree = ironwood_tree.ok_or_misc_error("missing Ironwood tree")?;
Some(IronwoodTrees {
size: ironwood_tree.count(),
})
} else {
None
};
let trees = GetBlockTrees {
sapling,
orchard,
ironwood,
};
let block_info_response = futs.next().await.expect("`futs` should not be empty");
let zakura_state::ReadResponse::BlockInfo(prev_block_info) =
block_info_response.map_misc_error()?
else {
unreachable!("unmatched response to a BlockInfo request");
};
let block_info_response = futs.next().await.expect("`futs` should not be empty");
let zakura_state::ReadResponse::BlockInfo(block_info) =
block_info_response.map_misc_error()?
else {
unreachable!("unmatched response to a BlockInfo request");
};
let delta = block_info.as_ref().and_then(|d| {
let value_pools = d.value_pools().constrain::<NegativeAllowed>().ok()?;
let prev_value_pools = prev_block_info
.map(|d| d.value_pools().constrain::<NegativeAllowed>())
.unwrap_or(Ok(ValueBalance::<NegativeAllowed>::zero()))
.ok()?;
(value_pools - prev_value_pools).ok()
});
let size = size.or(block_info.as_ref().map(|d| d.size() as usize));
Ok(GetBlockResponse::Object(Box::new(BlockObject {
hash,
confirmations,
height: Some(height),
version: Some(version),
merkle_root: Some(merkle_root),
time: Some(time),
nonce: Some(nonce),
solution: Some(solution),
bits: Some(bits),
difficulty: Some(difficulty),
n_tx: tx.len(),
tx,
trees,
chain_supply: block_info
.as_ref()
.map(|d| GetBlockchainInfoBalance::chain_supply(*d.value_pools())),
value_pools: block_info
.map(|d| GetBlockchainInfoBalance::value_pools(*d.value_pools(), delta)),
size: size.map(|size| size as i64),
block_commitments: Some(block_commitments),
final_sapling_root: Some(final_sapling_root),
final_orchard_root,
previous_block_hash: Some(previous_block_hash),
next_block_hash,
})))
} else {
Err("invalid verbosity value").map_error(server::error::LegacyCode::InvalidParameter)
}
}
async fn get_block_header(
&self,
hash_or_height: String,
verbose: Option<bool>,
) -> Result<GetBlockHeaderResponse> {
let verbose = verbose.unwrap_or(true);
let network = self.network.clone();
let hash_or_height =
HashOrHeight::new(&hash_or_height, self.latest_chain_tip.best_tip_height())
.map_error(server::error::LegacyCode::InvalidParameter)?;
let zakura_state::ReadResponse::BlockHeader {
header,
hash,
height,
next_block_hash,
} = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::BlockHeader(hash_or_height))
.await
.map_err(|_| "block height not in best chain")
.map_error(
if hash_or_height.hash().is_some() {
server::error::LegacyCode::InvalidAddressOrKey
} else {
server::error::LegacyCode::InvalidParameter
},
)?
else {
panic!("unexpected response to BlockHeader request")
};
let response = if !verbose {
GetBlockHeaderResponse::Raw(HexData(header.zcash_serialize_to_vec().map_misc_error()?))
} else {
let zakura_state::ReadResponse::SaplingTree(sapling_tree) = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::SaplingTree(hash.into()))
.await
.map_misc_error()?
else {
panic!("unexpected response to SaplingTree request")
};
let sapling_tree = sapling_tree.ok_or_misc_error("missing Sapling tree")?;
let zakura_state::ReadResponse::Depth(depth) = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::Depth(hash))
.await
.map_misc_error()?
else {
panic!("unexpected response to SaplingTree request")
};
const NOT_IN_BEST_CHAIN_CONFIRMATIONS: i64 = -1;
let confirmations = depth
.map(|depth| i64::from(depth) + 1)
.unwrap_or(NOT_IN_BEST_CHAIN_CONFIRMATIONS);
let mut nonce = *header.nonce;
nonce.reverse();
let sapling_activation = NetworkUpgrade::Sapling.activation_height(&network);
let sapling_tree_size = sapling_tree.count();
let final_sapling_root: [u8; 32] =
if sapling_activation.is_some() && height >= sapling_activation.unwrap() {
let mut root: [u8; 32] = sapling_tree.root().into();
root.reverse();
root
} else {
[0; 32]
};
let difficulty = header.difficulty_threshold.relative_to_network(&network);
let block_commitments = match header.commitment(&network, height).expect(
"Unexpected failure while parsing the blockcommitments field in get_block_header",
) {
Commitment::PreSaplingReserved(bytes) => bytes,
Commitment::FinalSaplingRoot(_) => final_sapling_root,
Commitment::ChainHistoryActivationReserved => [0; 32],
Commitment::ChainHistoryRoot(root) => root.bytes_in_display_order(),
Commitment::ChainHistoryBlockTxAuthCommitment(hash) => {
hash.bytes_in_display_order()
}
};
let block_header = BlockHeaderObject {
hash,
confirmations,
height,
version: header.version,
merkle_root: header.merkle_root,
block_commitments,
final_sapling_root,
sapling_tree_size,
time: header.time.timestamp(),
nonce,
solution: header.solution,
bits: header.difficulty_threshold,
difficulty,
previous_block_hash: header.previous_block_hash,
next_block_hash,
};
GetBlockHeaderResponse::Object(Box::new(block_header))
};
Ok(response)
}
fn get_best_block_hash(&self) -> Result<GetBlockHashResponse> {
self.latest_chain_tip
.best_tip_hash()
.map(GetBlockHashResponse)
.ok_or_misc_error("No blocks in state")
}
fn get_best_block_height_and_hash(&self) -> Result<GetBlockHeightAndHashResponse> {
self.latest_chain_tip
.best_tip_height_and_hash()
.map(|(height, hash)| GetBlockHeightAndHashResponse { height, hash })
.ok_or_misc_error("No blocks in state")
}
async fn get_mempool_info(&self) -> Result<GetMempoolInfoResponse> {
let mut mempool = self.mempool.clone();
let response = mempool
.ready()
.and_then(|service| service.call(mempool::Request::QueueStats))
.await
.map_misc_error()?;
if let mempool::Response::QueueStats {
size,
bytes,
usage,
fully_notified,
} = response
{
Ok(GetMempoolInfoResponse {
size,
bytes,
usage,
fully_notified,
})
} else {
unreachable!("unexpected response to QueueStats request")
}
}
async fn get_raw_mempool(&self, verbose: Option<bool>) -> Result<GetRawMempoolResponse> {
#[allow(unused)]
let verbose = verbose.unwrap_or(false);
use zakura_chain::block::MAX_BLOCK_BYTES;
let mut mempool = self.mempool.clone();
let request = if verbose {
mempool::Request::FullTransactions
} else {
mempool::Request::TransactionIds
};
let response = mempool
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
match response {
mempool::Response::FullTransactions {
mut transactions,
transaction_dependencies,
last_seen_tip_hash: _,
} => {
if verbose {
let transactions_by_id = transactions
.iter()
.map(|unmined_tx| (unmined_tx.transaction.id.mined_id(), unmined_tx))
.collect::<HashMap<_, _>>();
let map = transactions
.iter()
.map(|unmined_tx| {
(
unmined_tx.transaction.id.mined_id().encode_hex(),
get_raw_mempool::MempoolObject::from_verified_unmined_tx(
unmined_tx,
&transactions_by_id,
&transaction_dependencies,
),
)
})
.collect::<HashMap<_, _>>();
Ok(GetRawMempoolResponse::Verbose(map))
} else {
transactions.sort_by_cached_key(|tx| {
cmp::Reverse((
i64::from(tx.miner_fee) as u128 * MAX_BLOCK_BYTES as u128
/ tx.transaction.size as u128,
tx.transaction.id.mined_id(),
))
});
let tx_ids: Vec<String> = transactions
.iter()
.map(|unmined_tx| unmined_tx.transaction.id.mined_id().encode_hex())
.collect();
Ok(GetRawMempoolResponse::TxIds(tx_ids))
}
}
mempool::Response::TransactionIds(unmined_transaction_ids) => {
let mut tx_ids: Vec<String> = unmined_transaction_ids
.iter()
.map(|id| id.mined_id().encode_hex())
.collect();
tx_ids.sort();
Ok(GetRawMempoolResponse::TxIds(tx_ids))
}
_ => unreachable!("unmatched response to a transactionids request"),
}
}
async fn get_raw_transaction(
&self,
txid: String,
verbose: Option<u8>,
block_hash: Option<String>,
) -> Result<GetRawTransactionResponse> {
let mut mempool = self.mempool.clone();
let verbose = verbose.unwrap_or(0) != 0;
let txid = transaction::Hash::from_hex(txid)
.map_error(server::error::LegacyCode::InvalidAddressOrKey)?;
if block_hash.is_none() {
match mempool
.ready()
.and_then(|service| {
service.call(mempool::Request::TransactionsByMinedId([txid].into()))
})
.await
.map_misc_error()?
{
mempool::Response::Transactions(txns) => {
if let Some(tx) = txns.first() {
return Ok(if verbose {
GetRawTransactionResponse::Object(Box::new(
TransactionObject::from_transaction(
tx.transaction.clone(),
None,
None,
&self.network,
None,
None,
Some(false),
txid,
),
))
} else {
let hex = tx.transaction.clone().into();
GetRawTransactionResponse::Raw(hex)
});
}
}
_ => unreachable!("unmatched response to a `TransactionsByMinedId` request"),
};
}
let caller_block_context = if let Some(block_hash) = block_hash {
let block_hash = block::Hash::from_hex(block_hash)
.map_error(server::error::LegacyCode::InvalidAddressOrKey)?;
match self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::AnyChainTransactionIdsForBlock(
block_hash.into(),
))
.await
.map_misc_error()?
{
zakura_state::ReadResponse::AnyChainTransactionIdsForBlock(tx_ids) => {
let (ids, in_best_chain) = tx_ids.ok_or_error(
server::error::LegacyCode::InvalidAddressOrKey,
"block not found",
)?;
ids.iter().find(|id| **id == txid).ok_or_error(
server::error::LegacyCode::InvalidAddressOrKey,
"txid not found",
)?;
Some((block_hash, in_best_chain))
}
_ => {
unreachable!("unmatched response to a `AnyChainTransactionIdsForBlock` request")
}
}
} else {
None
};
match self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::AnyChainTransaction(txid))
.await
.map_misc_error()?
{
zakura_state::ReadResponse::AnyChainTransaction(Some(tx)) => Ok(if verbose {
if let Some((caller_block_hash, in_best_chain)) = caller_block_context {
let (raw_tx, height, confirmations, block_time) = match &tx {
AnyTx::Mined(mined) if in_best_chain => (
mined.tx.clone(),
Some(mined.height),
Some(mined.confirmations.into()),
Some(mined.block_time),
),
_ => {
let raw_tx: Arc<Transaction> = tx.into();
(raw_tx, None, None, None)
}
};
GetRawTransactionResponse::Object(Box::new(
TransactionObject::from_transaction(
raw_tx,
height,
confirmations,
&self.network,
block_time,
Some(caller_block_hash),
Some(in_best_chain),
txid,
),
))
} else {
match tx {
AnyTx::Mined(tx) => {
let block_hash = match self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::BestChainBlockHash(tx.height))
.await
.map_misc_error()?
{
zakura_state::ReadResponse::BlockHash(block_hash) => block_hash,
_ => {
unreachable!(
"unmatched response to a `BestChainBlockHash` request"
)
}
};
GetRawTransactionResponse::Object(Box::new(
TransactionObject::from_transaction(
tx.tx.clone(),
Some(tx.height),
Some(tx.confirmations.into()),
&self.network,
Some(tx.block_time),
block_hash,
Some(true),
txid,
),
))
}
AnyTx::Side((tx, block_hash)) => GetRawTransactionResponse::Object(
Box::new(TransactionObject::from_transaction(
tx.clone(),
None,
None,
&self.network,
None,
Some(block_hash),
Some(false),
txid,
)),
),
}
}
} else {
let tx: Arc<Transaction> = tx.into();
let hex = tx.into();
GetRawTransactionResponse::Raw(hex)
}),
zakura_state::ReadResponse::AnyChainTransaction(None) => {
Err("Transaction not found in mempool or best chain")
.map_error(server::error::LegacyCode::InvalidAddressOrKey)
}
_ => unreachable!("unmatched response to a `Transaction` read request"),
}
}
async fn z_get_treestate(&self, hash_or_height: String) -> Result<GetTreestateResponse> {
let mut read_state = self.read_state.clone();
let network = self.network.clone();
let hash_or_height =
HashOrHeight::new(&hash_or_height, self.latest_chain_tip.best_tip_height())
.map_error(server::error::LegacyCode::InvalidParameter)?;
let block = match read_state
.ready()
.and_then(|service| service.call(zakura_state::ReadRequest::Block(hash_or_height)))
.await
.map_misc_error()?
{
zakura_state::ReadResponse::Block(Some(block)) => block,
zakura_state::ReadResponse::Block(None) => {
return Err("the requested block is not in the main chain")
.map_error(server::error::LegacyCode::InvalidParameter);
}
_ => unreachable!("unmatched response to a block request"),
};
let hash = hash_or_height
.hash_or_else(|_| Some(block.hash()))
.expect("block hash");
let height = hash_or_height
.height_or_else(|_| block.coinbase_height())
.expect("verified blocks have a coinbase height");
let time = u32::try_from(block.header.time.timestamp())
.expect("Timestamps of valid blocks always fit into u32.");
let sapling = if network.is_nu_active(consensus::NetworkUpgrade::Sapling, height.into()) {
match read_state
.ready()
.and_then(|service| {
service.call(zakura_state::ReadRequest::SaplingTree(hash.into()))
})
.await
.map_misc_error()?
{
zakura_state::ReadResponse::SaplingTree(tree) => {
tree.map(|t| (t.to_rpc_bytes(), t.root().bytes_in_display_order().to_vec()))
}
_ => unreachable!("unmatched response to a Sapling tree request"),
}
} else {
None
};
let (sapling_tree, sapling_root) =
sapling.map_or((None, None), |(tree, root)| (Some(tree), Some(root)));
let orchard = if network.is_nu_active(consensus::NetworkUpgrade::Nu5, height.into()) {
match read_state
.ready()
.and_then(|service| {
service.call(zakura_state::ReadRequest::OrchardTree(hash.into()))
})
.await
.map_misc_error()?
{
zakura_state::ReadResponse::OrchardTree(tree) => {
tree.map(|t| (t.to_rpc_bytes(), t.root().bytes_in_display_order().to_vec()))
}
_ => unreachable!("unmatched response to an Orchard tree request"),
}
} else {
None
};
let (orchard_tree, orchard_root) =
orchard.map_or((None, None), |(tree, root)| (Some(tree), Some(root)));
let ironwood = if network.is_nu_active(consensus::NetworkUpgrade::Nu6_3, height.into()) {
match read_state
.ready()
.and_then(|service| {
service.call(zakura_state::ReadRequest::IronwoodTree(hash.into()))
})
.await
.map_misc_error()?
{
zakura_state::ReadResponse::IronwoodTree(tree) => {
tree.map(|t| (t.to_rpc_bytes(), t.root().bytes_in_display_order().to_vec()))
}
_ => unreachable!("unmatched response to an Ironwood tree request"),
}
} else {
None
};
let ironwood = ironwood.map_or_else(Treestate::default, |(tree, root)| {
Treestate::new(trees::Commitments::new(Some(root), Some(tree)))
});
Ok(GetTreestateResponse::new(
hash,
height,
time,
None,
Treestate::new(trees::Commitments::new(sapling_root, sapling_tree)),
Treestate::new(trees::Commitments::new(orchard_root, orchard_tree)),
ironwood,
))
}
async fn z_get_subtrees_by_index(
&self,
pool: String,
start_index: NoteCommitmentSubtreeIndex,
limit: Option<NoteCommitmentSubtreeIndex>,
) -> Result<GetSubtreesByIndexResponse> {
let mut read_state = self.read_state.clone();
const POOL_LIST: &[&str] = &["sapling", "orchard", "ironwood"];
if pool == "sapling" {
let request = zakura_state::ReadRequest::SaplingSubtrees { start_index, limit };
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
let subtrees = match response {
zakura_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees,
_ => unreachable!("unmatched response to a subtrees request"),
};
let subtrees = subtrees
.values()
.map(|subtree| SubtreeRpcData {
root: subtree.root.to_bytes().encode_hex(),
end_height: subtree.end_height,
})
.collect();
Ok(GetSubtreesByIndexResponse {
pool,
start_index,
subtrees,
})
} else if pool == "orchard" {
let request = zakura_state::ReadRequest::OrchardSubtrees { start_index, limit };
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
let subtrees = match response {
zakura_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees,
_ => unreachable!("unmatched response to a subtrees request"),
};
let subtrees = subtrees
.values()
.map(|subtree| SubtreeRpcData {
root: subtree.root.encode_hex(),
end_height: subtree.end_height,
})
.collect();
Ok(GetSubtreesByIndexResponse {
pool,
start_index,
subtrees,
})
} else if pool == "ironwood" {
let request = zakura_state::ReadRequest::IronwoodSubtrees { start_index, limit };
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
let subtrees = match response {
zakura_state::ReadResponse::IronwoodSubtrees(subtrees) => subtrees,
_ => unreachable!("unmatched response to a subtrees request"),
};
let subtrees = subtrees
.values()
.map(|subtree| SubtreeRpcData {
root: subtree.root.encode_hex(),
end_height: subtree.end_height,
})
.collect();
Ok(GetSubtreesByIndexResponse {
pool,
start_index,
subtrees,
})
} else {
Err(ErrorObject::owned(
server::error::LegacyCode::Misc.into(),
format!("invalid pool name, must be one of: {POOL_LIST:?}").as_str(),
None::<()>,
))
}
}
async fn get_address_tx_ids(&self, request: GetAddressTxIdsRequest) -> Result<Vec<String>> {
let mut read_state = self.read_state.clone();
let latest_chain_tip = self.latest_chain_tip.clone();
let height_range = build_height_range(
request.start,
request.end,
best_chain_tip_height(&latest_chain_tip)?,
)?;
let valid_addresses = request.valid_addresses()?;
let request = zakura_state::ReadRequest::TransactionIdsByAddresses {
addresses: valid_addresses,
height_range,
};
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
let hashes = match response {
zakura_state::ReadResponse::AddressesTransactionIds(hashes) => {
let mut last_tx_location = TransactionLocation::from_usize(Height(0), 0);
hashes
.iter()
.map(|(tx_loc, tx_id)| {
assert!(
*tx_loc > last_tx_location,
"Transactions were not in chain order:\n\
{tx_loc:?} {tx_id:?} was after:\n\
{last_tx_location:?}",
);
last_tx_location = *tx_loc;
tx_id.to_string()
})
.collect()
}
_ => unreachable!("unmatched response to a TransactionsByAddresses request"),
};
Ok(hashes)
}
async fn get_address_utxos(
&self,
utxos_request: GetAddressUtxosRequest,
) -> Result<GetAddressUtxosResponse> {
let mut read_state = self.read_state.clone();
let mut response_utxos = vec![];
let valid_addresses = utxos_request.valid_addresses()?;
let request = zakura_state::ReadRequest::UtxosByAddresses(valid_addresses);
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_misc_error()?;
let utxos = match response {
zakura_state::ReadResponse::AddressUtxos(utxos) => utxos,
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
};
let mut last_output_location = OutputLocation::from_usize(Height(0), 0, 0);
for utxo_data in utxos.utxos() {
let address = utxo_data.0;
let txid = *utxo_data.1;
let height = utxo_data.2.height();
let output_index = utxo_data.2.output_index();
let script = utxo_data.3.lock_script.clone();
let satoshis = u64::from(utxo_data.3.value);
let output_location = *utxo_data.2;
assert!(
output_location > last_output_location,
"UTXOs were not in chain order:\n\
{output_location:?} {address:?} {txid:?} was after:\n\
{last_output_location:?}",
);
let entry = Utxo {
address,
txid,
output_index,
script,
satoshis,
height,
};
response_utxos.push(entry);
last_output_location = output_location;
}
if !utxos_request.chain_info {
Ok(GetAddressUtxosResponse::Utxos(response_utxos))
} else {
let (height, hash) = utxos
.last_height_and_hash()
.ok_or_misc_error("No blocks in state")?;
Ok(GetAddressUtxosResponse::UtxosAndChainInfo(
GetAddressUtxosResponseObject {
utxos: response_utxos,
hash,
height,
},
))
}
}
fn stop(&self) -> Result<String> {
#[cfg(not(target_os = "windows"))]
if self.network.is_regtest() {
match nix::sys::signal::raise(nix::sys::signal::SIGINT) {
Ok(_) => Ok("Zakura server stopping".to_string()),
Err(error) => Err(ErrorObject::owned(
ErrorCode::InternalError.code(),
format!("Failed to shut down: {error}").as_str(),
None::<()>,
)),
}
} else {
Err(ErrorObject::borrowed(
ErrorCode::MethodNotFound.code(),
"stop is only available on regtest networks",
None,
))
}
#[cfg(target_os = "windows")]
Err(ErrorObject::borrowed(
ErrorCode::MethodNotFound.code(),
"stop is not available in windows targets",
None,
))
}
fn get_block_count(&self) -> Result<u32> {
best_chain_tip_height(&self.latest_chain_tip).map(|height| height.0)
}
async fn get_block_hash(&self, index: i32) -> Result<GetBlockHashResponse> {
let mut read_state = self.read_state.clone();
let latest_chain_tip = self.latest_chain_tip.clone();
let tip_height = best_chain_tip_height(&latest_chain_tip)?;
let height = height_from_signed_int(index, tip_height)?;
let request = zakura_state::ReadRequest::BestChainBlockHash(height);
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_error(server::error::LegacyCode::default())?;
match response {
zakura_state::ReadResponse::BlockHash(Some(hash)) => Ok(GetBlockHashResponse(hash)),
zakura_state::ReadResponse::BlockHash(None) => Err(ErrorObject::borrowed(
server::error::LegacyCode::InvalidParameter.into(),
"Block not found",
None,
)),
_ => unreachable!("unmatched response to a block request"),
}
}
async fn get_block_template(
&self,
parameters: Option<GetBlockTemplateParameters>,
) -> Result<GetBlockTemplateResponse> {
use types::get_block_template::{
check_parameters, check_synced_to_tip, fetch_chain_info, fetch_mempool_transactions,
validate_block_proposal, zip317::select_mempool_transactions,
};
let mempool = self.mempool.clone();
let mut latest_chain_tip = self.latest_chain_tip.clone();
let sync_status = self.gbt.sync_status();
let read_state = self.read_state.clone();
if let Some(HexData(block_proposal_bytes)) = parameters
.as_ref()
.and_then(GetBlockTemplateParameters::block_proposal_data)
{
return validate_block_proposal(
self.gbt.block_verifier_router(),
block_proposal_bytes,
&self.network,
latest_chain_tip,
sync_status,
)
.await;
}
check_parameters(¶meters)?;
let client_long_poll_id = parameters.as_ref().and_then(|params| params.long_poll_id);
let miner_params = self
.gbt
.miner_params()
.ok_or_error(0, "miner parameters are required for get_block_template")?;
let mut max_time_reached = false;
let (server_long_poll_id, chain_info, mempool_txs, mempool_tx_deps, submit_old) = loop {
check_synced_to_tip(&self.network, latest_chain_tip.clone(), sync_status.clone())?;
latest_chain_tip.mark_best_tip_seen();
let chain_info @ zakura_state::GetBlockTemplateChainInfo {
tip_hash,
tip_height,
max_time,
cur_time,
..
} = fetch_chain_info(read_state.clone()).await?;
let Some((mempool_txs, mempool_tx_deps)) =
fetch_mempool_transactions(mempool.clone(), tip_hash)
.await?
.or_else(|| client_long_poll_id.is_none().then(Default::default))
else {
continue;
};
let server_long_poll_id = LongPollInput::new(
tip_height,
tip_hash,
max_time,
mempool_txs.iter().map(|tx| tx.transaction.id),
)
.generate_id();
if Some(&server_long_poll_id) != client_long_poll_id.as_ref() || max_time_reached {
let submit_old = if max_time_reached {
Some(false)
} else {
client_long_poll_id
.as_ref()
.map(|old_long_poll_id| server_long_poll_id.submit_old(old_long_poll_id))
};
break (
server_long_poll_id,
chain_info,
mempool_txs,
mempool_tx_deps,
submit_old,
);
}
let wait_for_mempool_request =
tokio::time::sleep(Duration::from_secs(MEMPOOL_LONG_POLL_INTERVAL));
let mut wait_for_new_tip = latest_chain_tip.clone();
let wait_for_new_tip = wait_for_new_tip.best_tip_changed();
let precomputed_height = Height(chain_info.tip_height.0 + 2);
let wait_for_new_tip = async {
let precompute_coinbase = |network, height, params| {
tokio::task::spawn_blocking(move || {
TransactionTemplate::new_coinbase(&network, height, ¶ms, Amount::zero())
.expect("valid coinbase tx")
})
};
let precomputed_coinbase = precompute_coinbase(
self.network.clone(),
precomputed_height,
miner_params.clone(),
)
.await
.expect("valid coinbase tx");
let _ = wait_for_new_tip.await;
precomputed_coinbase
};
let duration_until_max_time = max_time.saturating_duration_since(cur_time);
let wait_for_max_time: OptionFuture<_> = if duration_until_max_time.seconds() > 0 {
Some(tokio::time::sleep(duration_until_max_time.to_std()))
} else {
None
}
.into();
tokio::select! {
biased;
_elapsed = wait_for_mempool_request => {
tracing::debug!(
?max_time,
?cur_time,
?server_long_poll_id,
?client_long_poll_id,
MEMPOOL_LONG_POLL_INTERVAL,
"checking for a new mempool change after waiting a few seconds"
);
}
precomputed_coinbase = wait_for_new_tip => {
let chain_info = fetch_chain_info(read_state.clone()).await?;
let server_long_poll_id = LongPollInput::new(
chain_info.tip_height,
chain_info.tip_hash,
chain_info.max_time,
vec![]
)
.generate_id();
let submit_old = client_long_poll_id
.as_ref()
.map(|old_long_poll_id| server_long_poll_id.submit_old(old_long_poll_id));
let next_height = chain_info.tip_height.next().map_misc_error()?;
let precomputed_coinbase = (next_height == precomputed_height)
.then_some(precomputed_coinbase);
return Ok(BlockTemplateResponse::new_internal(
&self.network,
precomputed_coinbase,
miner_params,
&chain_info,
server_long_poll_id,
vec![],
submit_old,
)
.into())
}
Some(_elapsed) = wait_for_max_time => {
tracing::info!(
?max_time,
?cur_time,
?server_long_poll_id,
?client_long_poll_id,
"returning from long poll because max time was reached"
);
max_time_reached = true;
}
}
};
tracing::debug!(
mempool_tx_hashes = ?mempool_txs
.iter()
.map(|tx| tx.transaction.id.mined_id())
.collect::<Vec<_>>(),
"selecting transactions for the template from the mempool"
);
let height = chain_info.tip_height.next().map_misc_error()?;
let mempool_txs = select_mempool_transactions(
&self.network,
height,
miner_params,
mempool_txs,
mempool_tx_deps,
);
tracing::debug!(
selected_mempool_tx_hashes = ?mempool_txs
.iter()
.map(|#[cfg(not(test))] tx, #[cfg(test)] (_, tx)| tx.transaction.id.mined_id())
.collect::<Vec<_>>(),
"selected transactions for the template from the mempool"
);
Ok(BlockTemplateResponse::new_internal(
&self.network,
None,
miner_params,
&chain_info,
server_long_poll_id,
mempool_txs,
submit_old,
)
.into())
}
async fn submit_block(
&self,
HexData(block_bytes): HexData,
_parameters: Option<SubmitBlockParameters>,
) -> Result<SubmitBlockResponse> {
let mut block_verifier_router = self.gbt.block_verifier_router();
let block: Block = match block_bytes.zcash_deserialize_into() {
Ok(block_bytes) => block_bytes,
Err(error) => {
tracing::info!(
?error,
"submit block failed: block bytes could not be deserialized into a structurally valid block"
);
return Ok(SubmitBlockErrorResponse::Rejected.into());
}
};
let height = block
.coinbase_height()
.ok_or_error(0, "coinbase height not found")?;
let block_hash = block.hash();
let block_verifier_router_response = block_verifier_router
.ready()
.await
.map_err(|error| ErrorObject::owned(0, error.to_string(), None::<()>))?
.call(zakura_consensus::Request::Commit(Arc::new(block)))
.await;
let chain_error = match block_verifier_router_response {
Ok(hash) => {
tracing::info!(?hash, ?height, "submit block accepted");
self.gbt
.advertise_mined_block(hash, height)
.map_error_with_prefix(0, "failed to send mined block to gossip task")?;
return Ok(SubmitBlockResponse::Accepted);
}
Err(box_error) => {
let error = box_error
.downcast::<RouterError>()
.map(|boxed_chain_error| *boxed_chain_error);
tracing::info!(
?error,
?block_hash,
?height,
"submit block failed verification"
);
error
}
};
let response = match chain_error {
Ok(source) if source.is_duplicate_request() => SubmitBlockErrorResponse::Duplicate,
Ok(_verify_chain_error) => SubmitBlockErrorResponse::Rejected,
Err(_unknown_error_type) => SubmitBlockErrorResponse::Rejected,
};
Ok(response.into())
}
async fn get_mining_info(&self) -> Result<GetMiningInfoResponse> {
let network = self.network.clone();
let mut read_state = self.read_state.clone();
let chain_tip = self.latest_chain_tip.clone();
let tip_height = chain_tip.best_tip_height().unwrap_or(Height(0)).0;
let mut current_block_tx = None;
if tip_height > 0 {
let mined_tx_ids = chain_tip.best_tip_mined_transaction_ids();
current_block_tx =
(!mined_tx_ids.is_empty()).then(|| mined_tx_ids.len().saturating_sub(1));
}
let solution_rate_fut = self.get_network_sol_ps(None, None);
let mut current_block_size = None;
if tip_height > 0 {
let request = zakura_state::ReadRequest::TipBlockSize;
let response: zakura_state::ReadResponse = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_error(server::error::LegacyCode::default())?;
current_block_size = match response {
zakura_state::ReadResponse::TipBlockSize(Some(block_size)) => Some(block_size),
_ => None,
};
}
Ok(GetMiningInfoResponse::new_internal(
tip_height,
current_block_size,
current_block_tx,
network,
solution_rate_fut.await?,
))
}
async fn get_network_sol_ps(
&self,
num_blocks: Option<i32>,
height: Option<i32>,
) -> Result<u64> {
let mut num_blocks = num_blocks.unwrap_or(DEFAULT_SOLUTION_RATE_WINDOW_SIZE);
if num_blocks < 1 {
num_blocks = i32::try_from(POW_AVERAGING_WINDOW).expect("fits in i32");
}
let num_blocks =
usize::try_from(num_blocks).expect("just checked for negatives, i32 fits in usize");
let height = height.and_then(|height| height.try_into_height().ok());
let mut read_state = self.read_state.clone();
let request = ReadRequest::SolutionRate { num_blocks, height };
let response = read_state
.ready()
.and_then(|service| service.call(request))
.await
.map_err(|error| ErrorObject::owned(0, error.to_string(), None::<()>))?;
let solution_rate = match response {
ReadResponse::SolutionRate(solution_rate) => solution_rate.unwrap_or(0),
_ => unreachable!("unmatched response to a solution rate request"),
};
Ok(solution_rate
.try_into()
.expect("per-second solution rate always fits in u64"))
}
async fn get_network_info(&self) -> Result<GetNetworkInfoResponse> {
let version = GetInfoResponse::version_from_string(&self.build_version)
.expect("invalid version string");
let subversion = self.user_agent.clone();
let protocol_version = zakura_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION.0;
let local_services = format!("{:016x}", PeerServices::NODE_NETWORK);
let timeoffset = 0;
let connections = self.address_book.recently_live_peers(Utc::now()).len();
let networks = vec![
NetworkInfo::new("ipv4".to_string(), false, true, "".to_string(), false),
NetworkInfo::new("ipv6".to_string(), false, true, "".to_string(), false),
NetworkInfo::new("onion".to_string(), false, false, "".to_string(), false),
];
let relay_fee = zakura_chain::transaction::zip317::MIN_MEMPOOL_TX_FEE_RATE as f64
/ (zakura_chain::amount::COIN as f64);
let local_addresses = vec![];
let warnings = "".to_string();
let response = GetNetworkInfoResponse {
version,
subversion,
protocol_version,
local_services,
timeoffset,
connections,
networks,
relay_fee,
local_addresses,
warnings,
};
Ok(response)
}
async fn get_peer_info(&self) -> Result<Vec<PeerInfo>> {
let address_book = self.address_book.clone();
Ok(address_book
.recently_live_peers(chrono::Utc::now())
.into_iter()
.map(PeerInfo::from)
.collect())
}
async fn ping(&self) -> Result<()> {
tracing::debug!("Receiving ping request via RPC");
Ok(())
}
async fn validate_address(&self, raw_address: String) -> Result<ValidateAddressResponse> {
let network = self.network.clone();
validate_address(network, raw_address)
}
async fn z_validate_address(&self, raw_address: String) -> Result<ZValidateAddressResponse> {
let network = self.network.clone();
z_validate_address(network, raw_address)
}
async fn get_block_subsidy(&self, height: Option<u32>) -> Result<GetBlockSubsidyResponse> {
let net = self.network.clone();
let height = match height {
Some(h) => Height(h),
None => best_chain_tip_height(&self.latest_chain_tip)?,
};
let subsidy = block_subsidy(height, &net).map_misc_error()?;
let (lockbox_streams, mut funding_streams): (Vec<_>, Vec<_>) =
funding_stream_values(height, &net, subsidy)
.map_misc_error()?
.into_iter()
.partition(|(receiver, _)| matches!(receiver, FundingStreamReceiver::Deferred));
let [lockbox_total, funding_streams_total] =
[&lockbox_streams, &funding_streams].map(|streams| {
streams
.iter()
.map(|&(_, amount)| amount)
.sum::<std::result::Result<Amount<_>, _>>()
.map(Zec::from)
.map_misc_error()
});
funding_streams.sort_by_key(|(receiver, _funding_stream)| {
ZCASHD_FUNDING_STREAM_ORDER
.iter()
.position(|zcashd_receiver| zcashd_receiver == receiver)
});
let is_nu6 = NetworkUpgrade::current(&net, height) == NetworkUpgrade::Nu6;
let [funding_streams, lockbox_streams] =
[funding_streams, lockbox_streams].map(|streams| {
streams
.into_iter()
.map(|(receiver, value)| {
let address = funding_stream_address(height, &net, receiver);
types::subsidy::FundingStream::new_internal(
is_nu6, receiver, value, address,
)
})
.collect()
});
Ok(GetBlockSubsidyResponse {
miner: miner_subsidy(height, &net, subsidy)
.map_misc_error()?
.into(),
founders: founders_reward(&net, height).into(),
funding_streams,
lockbox_streams,
funding_streams_total: funding_streams_total?,
lockbox_total: lockbox_total?,
total_block_subsidy: subsidy.into(),
})
}
async fn get_difficulty(&self) -> Result<f64> {
chain_tip_difficulty(self.network.clone(), self.read_state.clone(), false).await
}
async fn z_list_unified_receivers(
&self,
address: String,
) -> Result<ZListUnifiedReceiversResponse> {
use zcash_address::unified::Container;
let (network, unified_address): (
zcash_protocol::consensus::NetworkType,
zcash_address::unified::Address,
) = zcash_address::unified::Encoding::decode(address.clone().as_str())
.map_err(|error| ErrorObject::owned(0, error.to_string(), None::<()>))?;
let mut p2pkh = None;
let mut p2sh = None;
let mut orchard = None;
let mut sapling = None;
for item in unified_address.items() {
match item {
zcash_address::unified::Receiver::Orchard(_data) => {
let addr = zcash_address::unified::Address::try_from_items(vec![item])
.expect("using data already decoded as valid");
orchard = Some(addr.encode(&network));
}
zcash_address::unified::Receiver::Sapling(data) => {
let addr = zakura_chain::primitives::Address::try_from_sapling(network, data)
.map_error(server::error::LegacyCode::InvalidParameter)?;
sapling = Some(addr.payment_address().unwrap_or_default());
}
zcash_address::unified::Receiver::P2pkh(data) => {
let addr = zakura_chain::primitives::Address::try_from_transparent_p2pkh(
network, data,
)
.expect("using data already decoded as valid");
p2pkh = Some(addr.payment_address().unwrap_or_default());
}
zcash_address::unified::Receiver::P2sh(data) => {
let addr =
zakura_chain::primitives::Address::try_from_transparent_p2sh(network, data)
.expect("using data already decoded as valid");
p2sh = Some(addr.payment_address().unwrap_or_default());
}
_ => (),
}
}
Ok(ZListUnifiedReceiversResponse::new(
orchard, sapling, p2pkh, p2sh,
))
}
async fn invalidate_block(&self, block_hash: String) -> Result<()> {
let block_hash = block_hash
.parse()
.map_error(server::error::LegacyCode::InvalidParameter)?;
self.state
.clone()
.oneshot(zakura_state::Request::InvalidateBlock(block_hash))
.await
.map(|rsp| assert_eq!(rsp, zakura_state::Response::Invalidated(block_hash)))
.map_misc_error()
}
async fn reconsider_block(&self, block_hash: String) -> Result<Vec<block::Hash>> {
let block_hash = block_hash
.parse()
.map_error(server::error::LegacyCode::InvalidParameter)?;
self.state
.clone()
.oneshot(zakura_state::Request::ReconsiderBlock(block_hash))
.await
.map(|rsp| match rsp {
zakura_state::Response::Reconsidered(block_hashes) => block_hashes,
_ => unreachable!("unmatched response to a reconsider block request"),
})
.map_misc_error()
}
async fn generate(&self, num_blocks: u32) -> Result<Vec<Hash>> {
let mut rpc = self.clone();
let network = self.network.clone();
if !network.disable_pow() {
return Err(ErrorObject::borrowed(
0,
"generate is only supported on networks where PoW is disabled",
None,
));
}
let mut block_hashes = Vec::new();
for _ in 0..num_blocks {
rpc.gbt.randomize_coinbase_data();
let block_template = rpc
.get_block_template(None)
.await
.map_error(server::error::LegacyCode::default())?;
let GetBlockTemplateResponse::TemplateMode(block_template) = block_template else {
return Err(ErrorObject::borrowed(
0,
"error generating block template",
None,
));
};
let proposal_block = proposal_block_from_template(
&block_template,
BlockTemplateTimeSource::CurTime,
&network,
)
.map_error(server::error::LegacyCode::default())?;
let hex_proposal_block = HexData(
proposal_block
.zcash_serialize_to_vec()
.map_error(server::error::LegacyCode::default())?,
);
let r = rpc
.submit_block(hex_proposal_block, None)
.await
.map_error(server::error::LegacyCode::default())?;
match r {
SubmitBlockResponse::Accepted => { }
SubmitBlockResponse::ErrorResponse(response) => {
return Err(ErrorObject::owned(
server::error::LegacyCode::Misc.into(),
format!("block was rejected: {response:?}"),
None::<()>,
));
}
}
block_hashes.push(GetBlockHashResponse(proposal_block.hash()));
}
Ok(block_hashes)
}
async fn add_node(
&self,
addr: zakura_network::PeerSocketAddr,
command: AddNodeCommand,
) -> Result<()> {
if self.network.is_regtest() {
match command {
AddNodeCommand::Add => {
tracing::info!(?addr, "adding peer address to the address book");
if self.address_book.clone().add_peer(addr) {
Ok(())
} else {
return Err(ErrorObject::owned(
server::error::LegacyCode::ClientNodeAlreadyAdded.into(),
format!("peer address was already present in the address book: {addr}"),
None::<()>,
));
}
}
}
} else {
return Err(ErrorObject::owned(
ErrorCode::InvalidParams.code(),
"addnode command is only supported on regtest",
None::<()>,
));
}
}
fn openrpc(&self) -> openrpsee::openrpc::Response {
let mut generator = openrpsee::openrpc::Generator::new();
let methods = METHODS
.into_iter()
.map(|(name, method)| method.generate(&mut generator, name))
.collect();
Ok(openrpsee::openrpc::OpenRpc {
openrpc: "1.3.2",
info: openrpsee::openrpc::Info {
title: env!("CARGO_PKG_NAME"),
description: env!("CARGO_PKG_DESCRIPTION"),
version: env!("CARGO_PKG_VERSION"),
},
methods,
components: generator.into_components(),
})
}
async fn get_tx_out(
&self,
txid: String,
n: u32,
include_mempool: Option<bool>,
) -> Result<GetTxOutResponse> {
let txid = transaction::Hash::from_hex(txid)
.map_error(server::error::LegacyCode::InvalidParameter)?;
let outpoint = transparent::OutPoint {
hash: txid,
index: n,
};
if include_mempool.unwrap_or(true) {
let rsp = self
.mempool
.clone()
.oneshot(mempool::Request::UnspentOutput(outpoint))
.await
.map_misc_error()?;
match rsp {
mempool::Response::TransparentOutput(Some(CreatedOrSpent::Created {
output,
tx_version,
last_seen_hash,
})) => {
return Ok(GetTxOutResponse(Some(
types::transaction::OutputObject::from_output(
&output,
last_seen_hash.to_string(),
0,
tx_version,
false,
self.network(),
),
)))
}
mempool::Response::TransparentOutput(Some(CreatedOrSpent::Spent)) => {
return Ok(GetTxOutResponse(None))
}
mempool::Response::TransparentOutput(None) => {}
_ => unreachable!("unmatched response to an `UnspentOutput` request"),
};
}
let tip_rsp = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::Tip)
.await
.map_misc_error()?;
let best_block_hash = match tip_rsp {
zakura_state::ReadResponse::Tip(tip) => tip.ok_or_misc_error("No blocks in state")?.1,
_ => unreachable!("unmatched response to a `Tip` request"),
};
let rsp = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::Transaction(txid))
.await
.map_misc_error()?;
match rsp {
zakura_state::ReadResponse::Transaction(Some(tx)) => {
let outputs = tx.tx.outputs();
let index: usize = n.try_into().expect("u32 always fits in usize");
let output = match outputs.get(index) {
Some(output) => output,
None => return Ok(GetTxOutResponse(None)),
};
let is_spent = {
let rsp = self
.read_state
.clone()
.oneshot(zakura_state::ReadRequest::IsTransparentOutputSpent(
outpoint,
))
.await
.map_misc_error()?;
match rsp {
zakura_state::ReadResponse::IsTransparentOutputSpent(spent) => spent,
_ => unreachable!(
"unmatched response to an `IsTransparentOutputSpent` request"
),
}
};
if is_spent {
return Ok(GetTxOutResponse(None));
}
Ok(GetTxOutResponse(Some(
types::transaction::OutputObject::from_output(
output,
best_block_hash.to_string(),
tx.confirmations,
tx.tx.version(),
tx.tx.is_coinbase(),
self.network(),
),
)))
}
zakura_state::ReadResponse::Transaction(None) => Ok(GetTxOutResponse(None)),
_ => unreachable!("unmatched response to a `Transaction` request"),
}
}
}
pub fn best_chain_tip_height<Tip>(latest_chain_tip: &Tip) -> Result<Height>
where
Tip: ChainTip + Clone + Send + Sync + 'static,
{
latest_chain_tip
.best_tip_height()
.ok_or_misc_error("No blocks in state")
}
#[allow(clippy::too_many_arguments)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
pub struct GetInfoResponse {
#[getter(rename = "raw_version")]
version: u64,
build: String,
subversion: String,
#[serde(rename = "protocolversion")]
protocol_version: u32,
blocks: u32,
connections: usize,
#[serde(skip_serializing_if = "Option::is_none")]
proxy: Option<String>,
difficulty: f64,
testnet: bool,
#[serde(rename = "paytxfee")]
pay_tx_fee: f64,
#[serde(rename = "relayfee")]
relay_fee: f64,
errors: String,
#[serde(rename = "errorstimestamp")]
errors_timestamp: i64,
}
#[deprecated(note = "Use `GetInfoResponse` instead")]
pub use self::GetInfoResponse as GetInfo;
impl Default for GetInfoResponse {
fn default() -> Self {
GetInfoResponse {
version: 0,
build: "some build version".to_string(),
subversion: "some subversion".to_string(),
protocol_version: 0,
blocks: 0,
connections: 0,
proxy: None,
difficulty: 0.0,
testnet: false,
pay_tx_fee: 0.0,
relay_fee: 0.0,
errors: "no errors".to_string(),
errors_timestamp: Utc::now().timestamp(),
}
}
}
impl GetInfoResponse {
#[allow(clippy::too_many_arguments)]
#[deprecated(note = "Use `GetInfoResponse::new` instead")]
pub fn from_parts(
version: u64,
build: String,
subversion: String,
protocol_version: u32,
blocks: u32,
connections: usize,
proxy: Option<String>,
difficulty: f64,
testnet: bool,
pay_tx_fee: f64,
relay_fee: f64,
errors: String,
errors_timestamp: i64,
) -> Self {
Self {
version,
build,
subversion,
protocol_version,
blocks,
connections,
proxy,
difficulty,
testnet,
pay_tx_fee,
relay_fee,
errors,
errors_timestamp,
}
}
pub fn into_parts(
self,
) -> (
u64,
String,
String,
u32,
u32,
usize,
Option<String>,
f64,
bool,
f64,
f64,
String,
i64,
) {
(
self.version,
self.build,
self.subversion,
self.protocol_version,
self.blocks,
self.connections,
self.proxy,
self.difficulty,
self.testnet,
self.pay_tx_fee,
self.relay_fee,
self.errors,
self.errors_timestamp,
)
}
fn version_from_string(build_string: &str) -> Option<u64> {
let semver_version = semver::Version::parse(build_string.strip_prefix('v')?).ok()?;
let build_number = semver_version
.build
.as_str()
.split('.')
.next()
.and_then(|num_str| num_str.parse::<u64>().ok())
.unwrap_or_default();
let version_number = 1_000_000 * semver_version.major
+ 10_000 * semver_version.minor
+ 100 * semver_version.patch
+ build_number;
Some(version_number)
}
}
pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 6];
fn deserialize_blockchain_value_pool_balances<'de, D>(
deserializer: D,
) -> std::result::Result<BlockchainValuePoolBalances, D::Error>
where
D: serde::Deserializer<'de>,
{
let value_pools = Vec::<GetBlockchainInfoBalance>::deserialize(deserializer)?;
blockchain_value_pool_balances_from_vec(value_pools)
}
fn deserialize_optional_blockchain_value_pool_balances<'de, D>(
deserializer: D,
) -> std::result::Result<Option<BlockchainValuePoolBalances>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<Vec<GetBlockchainInfoBalance>>::deserialize(deserializer)?
.map(blockchain_value_pool_balances_from_vec)
.transpose()
}
fn blockchain_value_pool_balances_from_vec<E>(
mut value_pools: Vec<GetBlockchainInfoBalance>,
) -> std::result::Result<BlockchainValuePoolBalances, E>
where
E: serde::de::Error,
{
match value_pools.len() {
5 => {
let ironwood_delta = value_pools
.iter()
.any(|pool| pool.value_delta().is_some() || pool.value_delta_zat().is_some())
.then(Amount::zero);
value_pools.push(GetBlockchainInfoBalance::ironwood(
Amount::zero(),
ironwood_delta,
));
}
6 => {}
len => return Err(E::invalid_length(len, &"five or six value pool balances")),
}
value_pools
.try_into()
.map_err(|_| E::custom("invalid value pool balance count"))
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters)]
pub struct GetBlockchainInfoResponse {
chain: String,
#[getter(copy)]
blocks: Height,
#[getter(copy)]
headers: Height,
difficulty: f64,
#[serde(rename = "verificationprogress")]
verification_progress: f64,
#[serde(rename = "chainwork")]
chain_work: u64,
pruned: bool,
size_on_disk: u64,
commitments: u64,
#[serde(rename = "bestblockhash", with = "hex")]
#[getter(copy)]
best_block_hash: block::Hash,
#[serde(rename = "estimatedheight")]
#[getter(copy)]
estimated_height: Height,
#[serde(rename = "chainSupply")]
chain_supply: GetBlockchainInfoBalance,
#[serde(rename = "valuePools")]
#[serde(deserialize_with = "deserialize_blockchain_value_pool_balances")]
value_pools: BlockchainValuePoolBalances,
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
#[getter(copy)]
consensus: TipConsensusBranch,
}
impl Default for GetBlockchainInfoResponse {
fn default() -> Self {
Self {
chain: "main".to_string(),
blocks: Height(1),
best_block_hash: block::Hash([0; 32]),
estimated_height: Height(1),
chain_supply: GetBlockchainInfoBalance::chain_supply(Default::default()),
value_pools: GetBlockchainInfoBalance::zero_pools(),
upgrades: IndexMap::new(),
consensus: TipConsensusBranch {
chain_tip: ConsensusBranchIdHex(ConsensusBranchId::default()),
next_block: ConsensusBranchIdHex(ConsensusBranchId::default()),
},
headers: Height(1),
difficulty: 0.0,
verification_progress: 0.0,
chain_work: 0,
pruned: false,
size_on_disk: 0,
commitments: 0,
}
}
}
impl GetBlockchainInfoResponse {
#[allow(clippy::too_many_arguments)]
pub fn new(
chain: String,
blocks: Height,
best_block_hash: block::Hash,
estimated_height: Height,
chain_supply: GetBlockchainInfoBalance,
value_pools: BlockchainValuePoolBalances,
upgrades: IndexMap<ConsensusBranchIdHex, NetworkUpgradeInfo>,
consensus: TipConsensusBranch,
headers: Height,
difficulty: f64,
verification_progress: f64,
chain_work: u64,
pruned: bool,
size_on_disk: u64,
commitments: u64,
) -> Self {
Self {
chain,
blocks,
best_block_hash,
estimated_height,
chain_supply,
value_pools,
upgrades,
consensus,
headers,
difficulty,
verification_progress,
chain_work,
pruned,
size_on_disk,
commitments,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize, JsonSchema)]
#[serde(from = "DGetAddressBalanceRequest")]
pub struct GetAddressBalanceRequest {
addresses: Vec<String>,
}
impl From<DGetAddressBalanceRequest> for GetAddressBalanceRequest {
fn from(address_strings: DGetAddressBalanceRequest) -> Self {
match address_strings {
DGetAddressBalanceRequest::Addresses { addresses } => {
GetAddressBalanceRequest { addresses }
}
DGetAddressBalanceRequest::Address(address) => GetAddressBalanceRequest {
addresses: vec![address],
},
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, JsonSchema)]
#[serde(untagged)]
enum DGetAddressBalanceRequest {
Addresses { addresses: Vec<String> },
Address(String),
}
#[deprecated(note = "Use `GetAddressBalanceRequest` instead.")]
pub type AddressStrings = GetAddressBalanceRequest;
pub trait ValidateAddresses {
fn valid_addresses(&self) -> Result<HashSet<Address>> {
let valid_addresses: HashSet<Address> = self
.addresses()
.iter()
.map(|address| {
address
.parse()
.map_error(server::error::LegacyCode::InvalidAddressOrKey)
})
.collect::<Result<_>>()?;
Ok(valid_addresses)
}
fn addresses(&self) -> &[String];
}
impl ValidateAddresses for GetAddressBalanceRequest {
fn addresses(&self) -> &[String] {
&self.addresses
}
}
impl GetAddressBalanceRequest {
pub fn new(addresses: Vec<String>) -> GetAddressBalanceRequest {
GetAddressBalanceRequest { addresses }
}
#[deprecated(
note = "Use `AddressStrings::new` instead. Validity will be checked by the server."
)]
pub fn new_valid(addresses: Vec<String>) -> Result<GetAddressBalanceRequest> {
let req = Self { addresses };
req.valid_addresses()?;
Ok(req)
}
}
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialEq,
Hash,
serde::Serialize,
serde::Deserialize,
Getters,
new,
)]
pub struct GetAddressBalanceResponse {
balance: u64,
pub received: u64,
}
#[deprecated(note = "Use `GetAddressBalanceResponse` instead.")]
pub use self::GetAddressBalanceResponse as AddressBalance;
#[derive(
Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Getters, new, JsonSchema,
)]
#[serde(from = "DGetAddressUtxosRequest")]
pub struct GetAddressUtxosRequest {
addresses: Vec<String>,
#[serde(default)]
#[serde(rename = "chainInfo")]
chain_info: bool,
}
impl From<DGetAddressUtxosRequest> for GetAddressUtxosRequest {
fn from(request: DGetAddressUtxosRequest) -> Self {
match request {
DGetAddressUtxosRequest::Single(addr) => GetAddressUtxosRequest {
addresses: vec![addr],
chain_info: false,
},
DGetAddressUtxosRequest::Object {
addresses,
chain_info,
} => GetAddressUtxosRequest {
addresses,
chain_info,
},
}
}
}
#[derive(Debug, serde::Deserialize, JsonSchema)]
#[serde(untagged)]
enum DGetAddressUtxosRequest {
Single(String),
Object {
addresses: Vec<String>,
#[serde(default)]
#[serde(rename = "chainInfo")]
chain_info: bool,
},
}
impl ValidateAddresses for GetAddressUtxosRequest {
fn addresses(&self) -> &[String] {
&self.addresses
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ConsensusBranchIdHex(#[serde(with = "hex")] ConsensusBranchId);
impl ConsensusBranchIdHex {
pub fn new(consensus_branch_id: u32) -> Self {
ConsensusBranchIdHex(consensus_branch_id.into())
}
pub fn inner(&self) -> u32 {
self.0.into()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct NetworkUpgradeInfo {
name: NetworkUpgrade,
#[serde(rename = "activationheight")]
activation_height: Height,
status: NetworkUpgradeStatus,
}
impl NetworkUpgradeInfo {
pub fn from_parts(
name: NetworkUpgrade,
activation_height: Height,
status: NetworkUpgradeStatus,
) -> Self {
Self {
name,
activation_height,
status,
}
}
pub fn into_parts(self) -> (NetworkUpgrade, Height, NetworkUpgradeStatus) {
(self.name, self.activation_height, self.status)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum NetworkUpgradeStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "disabled")]
Disabled,
#[serde(rename = "pending")]
Pending,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TipConsensusBranch {
#[serde(rename = "chaintip")]
chain_tip: ConsensusBranchIdHex,
#[serde(rename = "nextblock")]
next_block: ConsensusBranchIdHex,
}
impl TipConsensusBranch {
pub fn from_parts(chain_tip: u32, next_block: u32) -> Self {
Self {
chain_tip: ConsensusBranchIdHex::new(chain_tip),
next_block: ConsensusBranchIdHex::new(next_block),
}
}
pub fn into_parts(self) -> (u32, u32) {
(self.chain_tip.inner(), self.next_block.inner())
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SendRawTransactionResponse(#[serde(with = "hex")] transaction::Hash);
#[deprecated(note = "Use `SendRawTransactionResponse` instead")]
pub use self::SendRawTransactionResponse as SentTransactionHash;
impl Default for SendRawTransactionResponse {
fn default() -> Self {
Self(transaction::Hash::from([0; 32]))
}
}
impl SendRawTransactionResponse {
pub fn new(hash: transaction::Hash) -> Self {
SendRawTransactionResponse(hash)
}
#[deprecated(note = "Use `SentTransactionHash::hash` instead")]
pub fn inner(&self) -> transaction::Hash {
self.hash()
}
pub fn hash(&self) -> transaction::Hash {
self.0
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetBlockResponse {
Raw(#[serde(with = "hex")] SerializedBlock),
Object(Box<BlockObject>),
}
#[deprecated(note = "Use `GetBlockResponse` instead")]
pub use self::GetBlockResponse as GetBlock;
impl Default for GetBlockResponse {
fn default() -> Self {
GetBlockResponse::Object(Box::new(BlockObject {
hash: block::Hash([0; 32]),
confirmations: 0,
height: None,
time: None,
n_tx: 0,
tx: Vec::new(),
trees: GetBlockTrees::default(),
size: None,
version: None,
merkle_root: None,
block_commitments: None,
final_sapling_root: None,
final_orchard_root: None,
nonce: None,
bits: None,
difficulty: None,
chain_supply: None,
value_pools: None,
previous_block_hash: None,
next_block_hash: None,
solution: None,
}))
}
}
#[allow(clippy::too_many_arguments)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
pub struct BlockObject {
#[getter(copy)]
#[serde(with = "hex")]
hash: block::Hash,
confirmations: i64,
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
height: Option<Height>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
version: Option<u32>,
#[serde(with = "opthex", rename = "merkleroot")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
merkle_root: Option<block::merkle::Root>,
#[serde(with = "opthex", rename = "blockcommitments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
block_commitments: Option<[u8; 32]>,
#[serde(with = "opthex", rename = "finalsaplingroot")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
final_sapling_root: Option<[u8; 32]>,
#[serde(with = "opthex", rename = "finalorchardroot")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
final_orchard_root: Option<[u8; 32]>,
#[serde(rename = "nTx")]
n_tx: usize,
tx: Vec<GetBlockTransaction>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
time: Option<i64>,
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
nonce: Option<[u8; 32]>,
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
solution: Option<Solution>,
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
bits: Option<CompactDifficulty>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getter(copy)]
difficulty: Option<f64>,
#[serde(rename = "chainSupply")]
#[serde(skip_serializing_if = "Option::is_none")]
chain_supply: Option<GetBlockchainInfoBalance>,
#[serde(rename = "valuePools")]
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_blockchain_value_pool_balances"
)]
value_pools: Option<BlockchainValuePoolBalances>,
#[getter(copy)]
trees: GetBlockTrees,
#[serde(rename = "previousblockhash", skip_serializing_if = "Option::is_none")]
#[serde(with = "opthex")]
#[getter(copy)]
previous_block_hash: Option<block::Hash>,
#[serde(rename = "nextblockhash", skip_serializing_if = "Option::is_none")]
#[serde(with = "opthex")]
#[getter(copy)]
next_block_hash: Option<block::Hash>,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetBlockTransaction {
Hash(#[serde(with = "hex")] transaction::Hash),
Object(Box<TransactionObject>),
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetBlockHeaderResponse {
Raw(hex_data::HexData),
Object(Box<BlockHeaderObject>),
}
#[deprecated(note = "Use `GetBlockHeaderResponse` instead")]
pub use self::GetBlockHeaderResponse as GetBlockHeader;
#[allow(clippy::too_many_arguments)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
pub struct BlockHeaderObject {
#[serde(with = "hex")]
#[getter(copy)]
hash: block::Hash,
confirmations: i64,
#[getter(copy)]
height: Height,
version: u32,
#[serde(with = "hex", rename = "merkleroot")]
#[getter(copy)]
merkle_root: block::merkle::Root,
#[serde(with = "hex", rename = "blockcommitments")]
#[getter(copy)]
block_commitments: [u8; 32],
#[serde(with = "hex", rename = "finalsaplingroot")]
#[getter(copy)]
final_sapling_root: [u8; 32],
#[serde(skip)]
sapling_tree_size: u64,
time: i64,
#[serde(with = "hex")]
#[getter(copy)]
nonce: [u8; 32],
#[serde(with = "hex")]
#[getter(copy)]
solution: Solution,
#[serde(with = "hex")]
#[getter(copy)]
bits: CompactDifficulty,
difficulty: f64,
#[serde(rename = "previousblockhash")]
#[serde(with = "hex")]
#[getter(copy)]
previous_block_hash: block::Hash,
#[serde(rename = "nextblockhash", skip_serializing_if = "Option::is_none")]
#[getter(copy)]
#[serde(with = "opthex")]
next_block_hash: Option<block::Hash>,
}
#[deprecated(note = "Use `BlockHeaderObject` instead")]
pub use BlockHeaderObject as GetBlockHeaderObject;
impl Default for GetBlockHeaderResponse {
fn default() -> Self {
GetBlockHeaderResponse::Object(Box::default())
}
}
impl Default for BlockHeaderObject {
fn default() -> Self {
let difficulty: ExpandedDifficulty = zakura_chain::work::difficulty::U256::one().into();
BlockHeaderObject {
hash: block::Hash([0; 32]),
confirmations: 0,
height: Height::MIN,
version: 4,
merkle_root: block::merkle::Root([0; 32]),
block_commitments: Default::default(),
final_sapling_root: Default::default(),
sapling_tree_size: Default::default(),
time: 0,
nonce: [0; 32],
solution: Solution::for_proposal(),
bits: difficulty.to_compact(),
difficulty: 1.0,
previous_block_hash: block::Hash([0; 32]),
next_block_hash: Some(block::Hash([0; 32])),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct GetBlockHashResponse(#[serde(with = "hex")] pub(crate) block::Hash);
impl GetBlockHashResponse {
pub fn new(hash: block::Hash) -> Self {
GetBlockHashResponse(hash)
}
pub fn hash(&self) -> block::Hash {
self.0
}
}
#[deprecated(note = "Use `GetBlockHashResponse` instead")]
pub use self::GetBlockHashResponse as GetBlockHash;
pub type Hash = GetBlockHashResponse;
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Getters, new)]
pub struct GetBlockHeightAndHashResponse {
#[getter(copy)]
height: block::Height,
#[getter(copy)]
hash: block::Hash,
}
#[deprecated(note = "Use `GetBlockHeightAndHashResponse` instead.")]
pub use GetBlockHeightAndHashResponse as GetBestBlockHeightAndHash;
impl Default for GetBlockHeightAndHashResponse {
fn default() -> Self {
Self {
height: block::Height::MIN,
hash: block::Hash([0; 32]),
}
}
}
impl Default for GetBlockHashResponse {
fn default() -> Self {
GetBlockHashResponse(block::Hash([0; 32]))
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetRawTransactionResponse {
Raw(#[serde(with = "hex")] SerializedTransaction),
Object(Box<TransactionObject>),
}
#[deprecated(note = "Use `GetRawTransactionResponse` instead")]
pub use self::GetRawTransactionResponse as GetRawTransaction;
impl Default for GetRawTransactionResponse {
fn default() -> Self {
Self::Object(Box::default())
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum GetAddressUtxosResponse {
Utxos(Vec<Utxo>),
UtxosAndChainInfo(GetAddressUtxosResponseObject),
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
pub struct GetAddressUtxosResponseObject {
utxos: Vec<Utxo>,
#[serde(with = "hex")]
#[getter(copy)]
hash: block::Hash,
#[getter(copy)]
height: block::Height,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
pub struct Utxo {
address: transparent::Address,
#[serde(with = "hex")]
#[getter(copy)]
txid: transaction::Hash,
#[serde(rename = "outputIndex")]
#[getter(copy)]
output_index: OutputIndex,
#[serde(with = "hex")]
script: transparent::Script,
satoshis: u64,
#[getter(copy)]
height: Height,
}
#[deprecated(note = "Use `Utxo` instead")]
pub use self::Utxo as GetAddressUtxos;
impl Default for Utxo {
fn default() -> Self {
Self {
address: transparent::Address::from_pub_key_hash(
zakura_chain::parameters::NetworkKind::default(),
[0u8; 20],
),
txid: transaction::Hash::from([0; 32]),
output_index: OutputIndex::from_u64(0),
script: transparent::Script::new(&[0u8; 10]),
satoshis: u64::default(),
height: Height(0),
}
}
}
impl Utxo {
#[deprecated(note = "Use `Utxo::new` instead")]
pub fn from_parts(
address: transparent::Address,
txid: transaction::Hash,
output_index: OutputIndex,
script: transparent::Script,
satoshis: u64,
height: Height,
) -> Self {
Utxo {
address,
txid,
output_index,
script,
satoshis,
height,
}
}
pub fn into_parts(
&self,
) -> (
transparent::Address,
transaction::Hash,
OutputIndex,
transparent::Script,
u64,
Height,
) {
(
self.address,
self.txid,
self.output_index,
self.script.clone(),
self.satoshis,
self.height,
)
}
}
#[derive(
Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Getters, new, JsonSchema,
)]
#[serde(from = "DGetAddressTxIdsRequest")]
pub struct GetAddressTxIdsRequest {
addresses: Vec<String>,
start: Option<u32>,
end: Option<u32>,
}
impl GetAddressTxIdsRequest {
#[deprecated(note = "Use `GetAddressTxIdsRequest::new` instead.")]
pub fn from_parts(addresses: Vec<String>, start: u32, end: u32) -> Self {
GetAddressTxIdsRequest {
addresses,
start: Some(start),
end: Some(end),
}
}
pub fn into_parts(&self) -> (Vec<String>, u32, u32) {
(
self.addresses.clone(),
self.start.unwrap_or(0),
self.end.unwrap_or(0),
)
}
}
impl From<DGetAddressTxIdsRequest> for GetAddressTxIdsRequest {
fn from(request: DGetAddressTxIdsRequest) -> Self {
match request {
DGetAddressTxIdsRequest::Single(addr) => GetAddressTxIdsRequest {
addresses: vec![addr],
start: None,
end: None,
},
DGetAddressTxIdsRequest::Object {
addresses,
start,
end,
} => GetAddressTxIdsRequest {
addresses,
start,
end,
},
}
}
}
#[derive(Debug, serde::Deserialize, JsonSchema)]
#[serde(untagged)]
enum DGetAddressTxIdsRequest {
Single(String),
Object {
addresses: Vec<String>,
start: Option<u32>,
end: Option<u32>,
},
}
impl ValidateAddresses for GetAddressTxIdsRequest {
fn addresses(&self) -> &[String] {
&self.addresses
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct GetBlockTrees {
#[serde(skip_serializing_if = "SaplingTrees::is_empty")]
sapling: SaplingTrees,
#[serde(skip_serializing_if = "OrchardTrees::is_empty")]
orchard: OrchardTrees,
#[serde(default, skip_serializing_if = "Option::is_none")]
ironwood: Option<IronwoodTrees>,
}
impl Default for GetBlockTrees {
fn default() -> Self {
GetBlockTrees {
sapling: SaplingTrees { size: 0 },
orchard: OrchardTrees { size: 0 },
ironwood: None,
}
}
}
impl GetBlockTrees {
pub fn new(sapling: u64, orchard: u64, ironwood: Option<u64>) -> Self {
GetBlockTrees {
sapling: SaplingTrees { size: sapling },
orchard: OrchardTrees { size: orchard },
ironwood: ironwood.map(|size| IronwoodTrees { size }),
}
}
pub fn sapling(self) -> u64 {
self.sapling.size
}
pub fn orchard(self) -> u64 {
self.orchard.size
}
pub fn ironwood(self) -> Option<u64> {
self.ironwood.map(|ironwood| ironwood.size)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SaplingTrees {
size: u64,
}
impl SaplingTrees {
fn is_empty(&self) -> bool {
self.size == 0
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct OrchardTrees {
size: u64,
}
impl OrchardTrees {
fn is_empty(&self) -> bool {
self.size == 0
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct IronwoodTrees {
size: u64,
}
fn build_height_range(
start: Option<u32>,
end: Option<u32>,
chain_height: Height,
) -> Result<RangeInclusive<Height>> {
let start = Height(start.unwrap_or(0)).min(chain_height);
let end = match end {
Some(0) | None => chain_height,
Some(val) => Height(val).min(chain_height),
};
if start > end {
return Err(ErrorObject::owned(
ErrorCode::InvalidParams.code(),
format!("start {start:?} must be less than or equal to end {end:?}"),
None::<()>,
));
}
Ok(start..=end)
}
pub fn height_from_signed_int(index: i32, tip_height: Height) -> Result<Height> {
if index >= 0 {
let height = index.try_into().map_err(|_| {
ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Index conversion failed",
None,
)
})?;
if height > tip_height.0 {
return Err(ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Provided index is greater than the current tip",
None,
));
}
Ok(Height(height))
} else {
let height = i32::try_from(tip_height.0)
.map_err(|_| {
ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Tip height conversion failed",
None,
)
})?
.checked_add(index + 1);
let sanitized_height = match height {
None => {
return Err(ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Provided index is not valid",
None,
));
}
Some(h) => {
if h < 0 {
return Err(ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Provided negative index ends up with a negative height",
None,
));
}
let h: u32 = h.try_into().map_err(|_| {
ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Height conversion failed",
None,
)
})?;
if h > tip_height.0 {
return Err(ErrorObject::borrowed(
ErrorCode::InvalidParams.code(),
"Provided index is greater than the current tip",
None,
));
}
h
}
};
Ok(Height(sanitized_height))
}
}
pub mod opthex {
use hex::{FromHex, ToHex};
use serde::{de, Deserialize, Deserializer, Serializer};
#[allow(missing_docs)]
pub fn serialize<S, T>(data: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: ToHex,
{
match data {
Some(data) => {
let s = data.encode_hex::<String>();
serializer.serialize_str(&s)
}
None => serializer.serialize_none(),
}
}
#[allow(missing_docs)]
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromHex,
{
let opt = Option::<String>::deserialize(deserializer)?;
match opt {
Some(s) => T::from_hex(&s)
.map(Some)
.map_err(|_e| de::Error::custom("failed to convert hex string")),
None => Ok(None),
}
}
}
pub mod arrayhex {
use serde::{Deserializer, Serializer};
use std::fmt;
#[allow(missing_docs)]
pub fn serialize<S, const N: usize>(data: &[u8; N], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex_string = hex::encode(data);
serializer.serialize_str(&hex_string)
}
#[allow(missing_docs)]
pub fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
where
D: Deserializer<'de>,
{
struct HexArrayVisitor<const N: usize>;
impl<const N: usize> serde::de::Visitor<'_> for HexArrayVisitor<N> {
type Value = [u8; N];
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a hex string representing exactly {N} bytes")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let vec = hex::decode(v).map_err(E::custom)?;
vec.clone().try_into().map_err(|_| {
E::invalid_length(vec.len(), &format!("expected {N} bytes").as_str())
})
}
}
deserializer.deserialize_str(HexArrayVisitor::<N>)
}
}
pub async fn chain_tip_difficulty<State>(
network: Network,
mut state: State,
should_use_default: bool,
) -> Result<f64>
where
State: ReadStateService,
{
let request = ReadRequest::ChainInfo;
let response = state
.ready()
.and_then(|service| service.call(request))
.await;
let response = match (should_use_default, response) {
(_, Ok(res)) => res,
(true, Err(_)) => {
return Ok((U256::from(network.target_difficulty_limit()) >> 128).as_u128() as f64);
}
(false, Err(error)) => return Err(ErrorObject::owned(0, error.to_string(), None::<()>)),
};
let chain_info = match response {
ReadResponse::ChainInfo(info) => info,
_ => unreachable!("unmatched response to a chain info request"),
};
let pow_limit: U256 = network.target_difficulty_limit().into();
let Some(difficulty) = chain_info.expected_difficulty.to_expanded() else {
return Ok(0.0);
};
let pow_limit = pow_limit >> 128;
let difficulty = U256::from(difficulty) >> 128;
let pow_limit = pow_limit.as_u128() as f64;
let difficulty = difficulty.as_u128() as f64;
Ok(pow_limit / difficulty)
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)]
pub enum AddNodeCommand {
#[serde(rename = "add")]
Add,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct GetTxOutResponse(Option<types::transaction::OutputObject>);