use crate::{
endpoints,
id::{self, types::AccountCredentialMessage},
protocol_level_tokens,
types::{
self, block_certificates,
chain_parameters::ChainParameters,
hashes::{self, BlockHash, TransactionHash, TransactionSignHash},
queries::ConsensusDetailedStatus,
smart_contracts::{
ContractContext, InstanceInfo, InvokeContractResult, ModuleReference, WasmModule,
},
transactions::{self, InitContractPayload, UpdateContractPayload, UpdateInstruction},
AbsoluteBlockHeight, AccountInfo, AccountPending, BlockItemSummary,
CredentialRegistrationID, Energy, Memo, Nonce, RegisteredData, SpecialTransactionOutcome,
TransactionStatus, UpdateSequenceNumber,
},
};
use anyhow::Context;
pub use concordium_base::common::upward::{self, Upward};
use concordium_base::{
base::{AccountIndex, BlockHeight, Epoch, GenesisIndex},
common::{
self,
types::{TransactionSignature, TransactionSignaturesV1, TransactionTime},
},
contracts_common::{
AccountAddress, AccountAddressParseError, Amount, ContractAddress, OwnedContractName,
OwnedParameter, OwnedReceiveName, ReceiveName,
},
hashes::HashFromStrError,
transactions::{BlockItem, EncodedPayload, PayloadLike},
};
pub use endpoints::{QueryError, QueryResult, RPCError, RPCResult};
use futures::{Stream, StreamExt, TryStreamExt};
pub use http::uri::Scheme;
use std::{collections::HashMap, num::ParseIntError, str::FromStr};
use tonic::IntoRequest;
pub use tonic::{
transport::{Endpoint, Error},
Code, Status,
};
use self::dry_run::WithRemainingQuota;
mod conversions;
pub mod dry_run;
#[path = "generated/mod.rs"]
#[allow(
clippy::large_enum_variant,
clippy::enum_variant_names,
clippy::derive_partial_eq_without_eq
)]
#[rustfmt::skip]
mod gen;
pub use gen::concordium::v2 as generated;
pub mod proto_schema_version;
#[derive(Clone, Debug)]
pub struct Client {
client: generated::queries_client::QueriesClient<tonic::transport::Channel>,
}
#[derive(Clone, Copy, Debug)]
pub struct QueryResponse<A> {
pub block_hash: BlockHash,
pub response: A,
}
impl<A> AsRef<A> for QueryResponse<A> {
fn as_ref(&self) -> &A {
&self.response
}
}
#[derive(Copy, Clone, Debug, derive_more::From, PartialEq, Eq)]
pub enum BlockIdentifier {
Best,
LastFinal,
Given(BlockHash),
AbsoluteHeight(AbsoluteBlockHeight),
RelativeHeight(RelativeBlockHeight),
}
#[derive(Debug, thiserror::Error)]
pub enum BlockIdentifierFromStrError {
#[error("The input is not recognized.")]
InvalidFormat,
#[error("The input is not a valid hash: {0}.")]
InvalidHash(#[from] HashFromStrError),
#[error("The input is not a valid unsigned integer: {0}.")]
InvalidInteger(#[from] ParseIntError),
}
impl std::fmt::Display for BlockIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlockIdentifier::Best => "best".fmt(f),
BlockIdentifier::LastFinal => "lastfinal".fmt(f),
BlockIdentifier::Given(bh) => bh.fmt(f),
BlockIdentifier::AbsoluteHeight(h) => write!(f, "@{h}"),
BlockIdentifier::RelativeHeight(rh) => {
write!(
f,
"@{}/{}{}",
rh.height,
rh.genesis_index,
if rh.restrict { "!" } else { "" }
)
}
}
}
}
impl std::str::FromStr for BlockIdentifier {
type Err = BlockIdentifierFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"best" => Ok(Self::Best),
"lastFinal" => Ok(Self::LastFinal),
"lastfinal" => Ok(Self::LastFinal),
_ => {
if let Some(rest) = s.strip_prefix('@') {
if let Some((height_str, gen_idx_str)) = rest.split_once('/') {
let height = BlockHeight::from_str(height_str)?;
if let Some(gen_idx) = gen_idx_str.strip_suffix('!') {
let genesis_index = GenesisIndex::from_str(gen_idx)?;
Ok(Self::RelativeHeight(RelativeBlockHeight {
genesis_index,
height,
restrict: true,
}))
} else {
let genesis_index = GenesisIndex::from_str(gen_idx_str)?;
Ok(Self::RelativeHeight(RelativeBlockHeight {
genesis_index,
height,
restrict: false,
}))
}
} else {
let h = AbsoluteBlockHeight::from_str(rest)?;
Ok(Self::AbsoluteHeight(h))
}
} else {
let h = BlockHash::from_str(s)?;
Ok(Self::Given(h))
}
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct RelativeBlockHeight {
pub genesis_index: types::GenesisIndex,
pub height: types::BlockHeight,
pub restrict: bool,
}
#[derive(Copy, Clone, Debug, derive_more::From, derive_more::Display)]
pub enum AccountIdentifier {
#[display(fmt = "{_0}")]
Address(AccountAddress),
#[display(fmt = "{_0}")]
CredId(CredentialRegistrationID),
#[display(fmt = "{_0}")]
Index(crate::types::AccountIndex),
}
impl FromStr for AccountIdentifier {
type Err = AccountAddressParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(ai) = s.parse::<crate::types::AccountIndex>() {
return Ok(Self::Index(ai));
}
if let Ok(cid) = s.parse::<CredentialRegistrationID>() {
return Ok(Self::CredId(cid));
}
s.parse().map(Self::Address)
}
}
#[derive(Debug, Copy, Clone)]
pub struct SpecifiedEpoch {
pub genesis_index: types::GenesisIndex,
pub epoch: types::Epoch,
}
#[derive(Copy, Clone, Debug, derive_more::From)]
pub enum EpochIdentifier {
Specified(SpecifiedEpoch),
Block(BlockIdentifier),
}
#[derive(Debug, thiserror::Error)]
pub enum EpochIdentifierFromStrError {
#[error("The input is not recognized.")]
InvalidFormat,
#[error("The genesis index is not a valid unsigned integer")]
InvalidGenesis,
#[error("The epoch index is not a valid unsigned integer")]
InvalidEpoch,
#[error("The input is not a valid block identifier: {0}.")]
InvalidBlockIdentifier(#[from] BlockIdentifierFromStrError),
}
impl std::str::FromStr for EpochIdentifier {
type Err = EpochIdentifierFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(rest) = s.strip_prefix('%') {
if let Some((gen_idx_str, epoch_str)) = rest.split_once(',') {
let genesis_index = GenesisIndex::from_str(gen_idx_str)
.map_err(|_| EpochIdentifierFromStrError::InvalidGenesis)?;
let epoch = Epoch::from_str(epoch_str)
.map_err(|_| EpochIdentifierFromStrError::InvalidEpoch)?;
Ok(Self::Specified(SpecifiedEpoch {
genesis_index,
epoch,
}))
} else {
Err(EpochIdentifierFromStrError::InvalidFormat)
}
} else {
Ok(Self::Block(BlockIdentifier::from_str(s)?))
}
}
}
impl IntoRequest<generated::EpochRequest> for &EpochIdentifier {
fn into_request(self) -> tonic::Request<generated::EpochRequest> {
tonic::Request::new((*self).into())
}
}
impl From<EpochIdentifier> for generated::EpochRequest {
fn from(ei: EpochIdentifier) -> Self {
match ei {
EpochIdentifier::Specified(SpecifiedEpoch {
genesis_index,
epoch,
}) => generated::EpochRequest {
epoch_request_input: Some(
generated::epoch_request::EpochRequestInput::RelativeEpoch(
generated::epoch_request::RelativeEpoch {
genesis_index: Some(generated::GenesisIndex {
value: genesis_index.height,
}),
epoch: Some(generated::Epoch { value: epoch.epoch }),
},
),
),
},
EpochIdentifier::Block(bi) => generated::EpochRequest {
epoch_request_input: Some(generated::epoch_request::EpochRequestInput::BlockHash(
(&bi).into(),
)),
},
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct FinalizedBlockInfo {
pub block_hash: BlockHash,
pub height: AbsoluteBlockHeight,
}
impl From<&BlockIdentifier> for generated::BlockHashInput {
fn from(bi: &BlockIdentifier) -> Self {
let block_hash_input = match bi {
BlockIdentifier::Best => {
generated::block_hash_input::BlockHashInput::Best(Default::default())
}
BlockIdentifier::LastFinal => {
generated::block_hash_input::BlockHashInput::LastFinal(Default::default())
}
BlockIdentifier::Given(h) => {
generated::block_hash_input::BlockHashInput::Given(generated::BlockHash {
value: h.as_ref().to_vec(),
})
}
&BlockIdentifier::AbsoluteHeight(h) => {
generated::block_hash_input::BlockHashInput::AbsoluteHeight(h.into())
}
&BlockIdentifier::RelativeHeight(h) => {
generated::block_hash_input::BlockHashInput::RelativeHeight(h.into())
}
};
generated::BlockHashInput {
block_hash_input: Some(block_hash_input),
}
}
}
impl IntoRequest<generated::BlockHashInput> for &BlockIdentifier {
fn into_request(self) -> tonic::Request<generated::BlockHashInput> {
tonic::Request::new(self.into())
}
}
impl From<&AccountAddress> for generated::AccountAddress {
fn from(addr: &AccountAddress) -> Self {
generated::AccountAddress {
value: concordium_base::common::to_bytes(addr),
}
}
}
impl From<AccountAddress> for generated::AccountAddress {
fn from(addr: AccountAddress) -> Self {
generated::AccountAddress {
value: common::to_bytes(&addr),
}
}
}
impl From<&super::types::Address> for generated::Address {
fn from(addr: &super::types::Address) -> Self {
let ty = match addr {
super::types::Address::Account(account) => {
generated::address::Type::Account(account.into())
}
super::types::Address::Contract(contract) => {
generated::address::Type::Contract(contract.into())
}
};
generated::Address { r#type: Some(ty) }
}
}
impl From<&Memo> for generated::Memo {
fn from(v: &Memo) -> Self {
Self {
value: v.as_ref().clone(),
}
}
}
impl<'a> From<ReceiveName<'a>> for generated::ReceiveName {
fn from(a: ReceiveName<'a>) -> Self {
generated::ReceiveName {
value: a.get_chain_name().to_string(),
}
}
}
impl From<&RegisteredData> for generated::RegisteredData {
fn from(v: &RegisteredData) -> Self {
Self {
value: v.as_ref().clone(),
}
}
}
impl From<&[u8]> for generated::Parameter {
fn from(a: &[u8]) -> Self {
generated::Parameter { value: a.to_vec() }
}
}
impl From<&TransactionHash> for generated::TransactionHash {
fn from(th: &TransactionHash) -> Self {
generated::TransactionHash { value: th.to_vec() }
}
}
impl From<&AccountIdentifier> for generated::AccountIdentifierInput {
fn from(ai: &AccountIdentifier) -> Self {
let account_identifier_input = match ai {
AccountIdentifier::Address(addr) => {
generated::account_identifier_input::AccountIdentifierInput::Address(addr.into())
}
AccountIdentifier::CredId(credid) => {
let credid = generated::CredentialRegistrationId {
value: concordium_base::common::to_bytes(credid),
};
generated::account_identifier_input::AccountIdentifierInput::CredId(credid)
}
AccountIdentifier::Index(index) => {
generated::account_identifier_input::AccountIdentifierInput::AccountIndex(
(*index).into(),
)
}
};
generated::AccountIdentifierInput {
account_identifier_input: Some(account_identifier_input),
}
}
}
impl From<&ModuleReference> for generated::ModuleRef {
fn from(mr: &ModuleReference) -> Self {
Self { value: mr.to_vec() }
}
}
impl From<ModuleReference> for generated::ModuleRef {
fn from(mr: ModuleReference) -> Self {
Self { value: mr.to_vec() }
}
}
impl From<&WasmModule> for generated::VersionedModuleSource {
fn from(v: &WasmModule) -> Self {
Self {
module: Some(match v.version {
types::smart_contracts::WasmVersion::V0 => {
generated::versioned_module_source::Module::V0(
generated::versioned_module_source::ModuleSourceV0 {
value: v.source.as_ref().clone(),
},
)
}
types::smart_contracts::WasmVersion::V1 => {
generated::versioned_module_source::Module::V1(
generated::versioned_module_source::ModuleSourceV1 {
value: v.source.as_ref().clone(),
},
)
}
}),
}
}
}
impl From<&OwnedContractName> for generated::InitName {
fn from(v: &OwnedContractName) -> Self {
Self {
value: v.as_contract_name().get_chain_name().to_string(),
}
}
}
impl From<&OwnedReceiveName> for generated::ReceiveName {
fn from(v: &OwnedReceiveName) -> Self {
Self {
value: v.as_receive_name().get_chain_name().to_string(),
}
}
}
impl From<&OwnedParameter> for generated::Parameter {
fn from(v: &OwnedParameter) -> Self {
Self {
value: v.as_ref().to_vec(),
}
}
}
impl From<&InitContractPayload> for generated::InitContractPayload {
fn from(v: &InitContractPayload) -> Self {
Self {
amount: Some(v.amount.into()),
module_ref: Some(v.mod_ref.into()),
init_name: Some((&v.init_name).into()),
parameter: Some((&v.param).into()),
}
}
}
impl From<&UpdateContractPayload> for generated::UpdateContractPayload {
fn from(v: &UpdateContractPayload) -> Self {
Self {
amount: Some(v.amount.into()),
address: Some(v.address.into()),
receive_name: Some((&v.receive_name).into()),
parameter: Some((&v.message).into()),
}
}
}
impl From<&ContractAddress> for generated::ContractAddress {
fn from(ca: &ContractAddress) -> Self {
Self {
index: ca.index,
subindex: ca.subindex,
}
}
}
impl From<Nonce> for generated::SequenceNumber {
fn from(v: Nonce) -> Self {
generated::SequenceNumber { value: v.nonce }
}
}
impl From<UpdateSequenceNumber> for generated::UpdateSequenceNumber {
fn from(v: UpdateSequenceNumber) -> Self {
generated::UpdateSequenceNumber { value: v.number }
}
}
impl From<Energy> for generated::Energy {
fn from(v: Energy) -> Self {
generated::Energy { value: v.energy }
}
}
impl From<TransactionTime> for generated::TransactionTime {
fn from(v: TransactionTime) -> Self {
generated::TransactionTime { value: v.seconds }
}
}
impl From<&Amount> for generated::Amount {
fn from(v: &Amount) -> Self {
Self { value: v.micro_ccd }
}
}
impl From<Amount> for generated::Amount {
fn from(v: Amount) -> Self {
Self { value: v.micro_ccd }
}
}
impl
From<
&AccountCredentialMessage<
id::constants::IpPairing,
id::constants::ArCurve,
id::constants::AttributeKind,
>,
> for generated::CredentialDeployment
{
fn from(
v: &AccountCredentialMessage<
id::constants::IpPairing,
id::constants::ArCurve,
id::constants::AttributeKind,
>,
) -> Self {
Self {
message_expiry: Some(v.message_expiry.into()),
payload: Some(generated::credential_deployment::Payload::RawPayload(
common::to_bytes(&v.credential),
)),
}
}
}
impl From<&UpdateInstruction> for generated::UpdateInstruction {
fn from(v: &UpdateInstruction) -> Self {
Self {
signatures: Some(generated::SignatureMap {
signatures: {
let mut hm = HashMap::new();
for (key_idx, sig) in v.signatures.signatures.iter() {
hm.insert(
key_idx.index.into(),
generated::Signature {
value: sig.sig.to_owned(),
},
);
}
hm
},
}),
header: Some(generated::UpdateInstructionHeader {
sequence_number: Some(v.header.seq_number.into()),
effective_time: Some(v.header.effective_time.into()),
timeout: Some(v.header.timeout.into()),
}),
payload: Some(generated::UpdateInstructionPayload {
payload: Some(generated::update_instruction_payload::Payload::RawPayload(
common::to_bytes(&v.payload),
)),
}),
}
}
}
impl IntoRequest<generated::AccountInfoRequest> for (&AccountIdentifier, &BlockIdentifier) {
fn into_request(self) -> tonic::Request<generated::AccountInfoRequest> {
let ai = generated::AccountInfoRequest {
block_hash: Some(self.1.into()),
account_identifier: Some(self.0.into()),
};
tonic::Request::new(ai)
}
}
impl IntoRequest<generated::AncestorsRequest> for (&BlockIdentifier, u64) {
fn into_request(self) -> tonic::Request<generated::AncestorsRequest> {
let ar = generated::AncestorsRequest {
block_hash: Some(self.0.into()),
amount: self.1,
};
tonic::Request::new(ar)
}
}
impl IntoRequest<generated::ModuleSourceRequest> for (&ModuleReference, &BlockIdentifier) {
fn into_request(self) -> tonic::Request<generated::ModuleSourceRequest> {
let r = generated::ModuleSourceRequest {
block_hash: Some(self.1.into()),
module_ref: Some(self.0.into()),
};
tonic::Request::new(r)
}
}
impl IntoRequest<generated::InstanceInfoRequest> for (ContractAddress, &BlockIdentifier) {
fn into_request(self) -> tonic::Request<generated::InstanceInfoRequest> {
let r = generated::InstanceInfoRequest {
block_hash: Some(self.1.into()),
address: Some(self.0.into()),
};
tonic::Request::new(r)
}
}
impl<V: Into<Vec<u8>>> IntoRequest<generated::InstanceStateLookupRequest>
for (ContractAddress, &BlockIdentifier, V)
{
fn into_request(self) -> tonic::Request<generated::InstanceStateLookupRequest> {
let r = generated::InstanceStateLookupRequest {
block_hash: Some(self.1.into()),
address: Some(self.0.into()),
key: self.2.into(),
};
tonic::Request::new(r)
}
}
impl IntoRequest<generated::TransactionHash> for &TransactionHash {
fn into_request(self) -> tonic::Request<generated::TransactionHash> {
tonic::Request::new(self.into())
}
}
impl IntoRequest<generated::AccountIdentifierInput> for &AccountIdentifier {
fn into_request(self) -> tonic::Request<generated::AccountIdentifierInput> {
tonic::Request::new(self.into())
}
}
impl IntoRequest<generated::AccountAddress> for &AccountAddress {
fn into_request(self) -> tonic::Request<generated::AccountAddress> {
tonic::Request::new(self.into())
}
}
impl From<transactions::TransactionHeader> for generated::AccountTransactionHeader {
fn from(v: transactions::TransactionHeader) -> Self {
(&v).into()
}
}
impl From<&transactions::TransactionHeader> for generated::AccountTransactionHeader {
fn from(v: &transactions::TransactionHeader) -> Self {
Self {
sender: Some(generated::AccountAddress::from(v.sender)),
sequence_number: Some(v.nonce.into()),
energy_amount: Some(v.energy_amount.into()),
expiry: Some(v.expiry.into()),
}
}
}
impl From<&transactions::TransactionHeaderV1> for generated::AccountTransactionHeaderV1 {
fn from(v: &transactions::TransactionHeaderV1) -> Self {
Self {
sender: Some(generated::AccountAddress::from(v.sender)),
sponsor: v.sponsor.map(generated::AccountAddress::from),
sequence_number: Some(v.nonce.into()),
energy_amount: Some(v.energy_amount.into()),
expiry: Some(v.expiry.into()),
}
}
}
impl From<TransactionSignature> for generated::AccountTransactionSignature {
fn from(v: TransactionSignature) -> Self {
(&v).into()
}
}
impl From<&TransactionSignature> for generated::AccountTransactionSignature {
fn from(v: &TransactionSignature) -> Self {
Self {
signatures: {
let mut cred_map: HashMap<u32, generated::AccountSignatureMap> = HashMap::new();
for (cred_idx, sig_map) in v.signatures.iter() {
let mut acc_sig_map: HashMap<u32, generated::Signature> = HashMap::new();
for (key_idx, sig) in sig_map.iter() {
acc_sig_map.insert(
key_idx.0.into(),
generated::Signature {
value: sig.sig.to_owned(),
},
);
}
cred_map.insert(
cred_idx.index.into(),
generated::AccountSignatureMap {
signatures: acc_sig_map,
},
);
}
cred_map
},
}
}
}
impl From<&TransactionSignaturesV1> for generated::AccountTransactionV1Signatures {
fn from(v: &TransactionSignaturesV1) -> Self {
Self {
sender_signatures: Some(v.sender.to_owned().into()),
sponsor_signatures: v.sponsor.to_owned().map(|s| s.into()),
}
}
}
impl IntoRequest<generated::PreAccountTransaction>
for (&transactions::TransactionHeader, &transactions::Payload)
{
fn into_request(self) -> tonic::Request<generated::PreAccountTransaction> {
let request = generated::PreAccountTransaction {
header: Some(self.0.into()),
payload: Some(generated::AccountTransactionPayload {
payload: Some(generated::account_transaction_payload::Payload::RawPayload(
self.1.encode().into(),
)),
}),
};
tonic::Request::new(request)
}
}
impl<P: PayloadLike> IntoRequest<generated::SendBlockItemRequest> for &transactions::BlockItem<P> {
fn into_request(self) -> tonic::Request<generated::SendBlockItemRequest> {
let request = match self {
transactions::BlockItem::AccountTransaction(v) => {
generated::SendBlockItemRequest {
block_item: Some(
generated::send_block_item_request::BlockItem::AccountTransaction(
generated::AccountTransaction {
signature: Some((&v.signature).into()),
header: Some((&v.header).into()),
payload: {
let atp = generated::AccountTransactionPayload{
payload: Some(generated::account_transaction_payload::Payload::RawPayload(v.payload.encode().into())),
};
Some(atp)
},
},
),
),
}
}
transactions::BlockItem::CredentialDeployment(v) => generated::SendBlockItemRequest {
block_item: Some(
generated::send_block_item_request::BlockItem::CredentialDeployment(
v.as_ref().into(),
),
),
},
transactions::BlockItem::UpdateInstruction(v) => generated::SendBlockItemRequest {
block_item: Some(
generated::send_block_item_request::BlockItem::UpdateInstruction(v.into()),
),
},
transactions::BlockItem::AccountTransactionV1(v) => {
generated::SendBlockItemRequest {
block_item: Some(
generated::send_block_item_request::BlockItem::AccountTransactionV1(
generated::AccountTransactionV1 {
signatures: Some((&v.signatures).into()),
header: Some((&v.header).into()),
payload: {
let atp = generated::AccountTransactionPayload{
payload: Some(generated::account_transaction_payload::Payload::RawPayload(v.payload.encode().into())),
};
Some(atp)
},
},
),
),
}
}
};
tonic::Request::new(request)
}
}
impl IntoRequest<generated::InvokeInstanceRequest> for (&BlockIdentifier, &ContractContext) {
fn into_request(self) -> tonic::Request<generated::InvokeInstanceRequest> {
let (block, context) = self;
tonic::Request::new(generated::InvokeInstanceRequest {
block_hash: Some(block.into()),
invoker: context.invoker.as_ref().map(|a| a.into()),
instance: Some((&context.contract).into()),
amount: Some(context.amount.into()),
entrypoint: Some(context.method.as_receive_name().into()),
parameter: Some(context.parameter.as_ref().into()),
energy: context.energy.map(From::from),
})
}
}
impl IntoRequest<generated::PoolInfoRequest> for (&BlockIdentifier, types::BakerId) {
fn into_request(self) -> tonic::Request<generated::PoolInfoRequest> {
let req = generated::PoolInfoRequest {
block_hash: Some(self.0.into()),
baker: Some(self.1.into()),
};
tonic::Request::new(req)
}
}
impl IntoRequest<generated::BakerId> for types::BakerId {
fn into_request(self) -> tonic::Request<generated::BakerId> {
tonic::Request::new(generated::BakerId {
value: self.id.index,
})
}
}
impl IntoRequest<generated::BlocksAtHeightRequest> for &endpoints::BlocksAtHeightInput {
fn into_request(self) -> tonic::Request<generated::BlocksAtHeightRequest> {
tonic::Request::new(self.into())
}
}
impl IntoRequest<generated::GetPoolDelegatorsRequest> for (&BlockIdentifier, types::BakerId) {
fn into_request(self) -> tonic::Request<generated::GetPoolDelegatorsRequest> {
let req = generated::GetPoolDelegatorsRequest {
block_hash: Some(self.0.into()),
baker: Some(self.1.into()),
};
tonic::Request::new(req)
}
}
impl TryFrom<crate::v2::generated::BannedPeer> for types::network::BannedPeer {
type Error = anyhow::Error;
fn try_from(value: crate::v2::generated::BannedPeer) -> Result<Self, Self::Error> {
Ok(types::network::BannedPeer(
<std::net::IpAddr as std::str::FromStr>::from_str(&value.ip_address.require()?.value)?,
))
}
}
impl TryFrom<generated::IpSocketAddress> for std::net::SocketAddr {
type Error = anyhow::Error;
fn try_from(value: generated::IpSocketAddress) -> Result<Self, Self::Error> {
Ok(std::net::SocketAddr::new(
<std::net::IpAddr as std::str::FromStr>::from_str(&value.ip.require()?.value)?,
value.port.require()?.value as u16,
))
}
}
impl IntoRequest<crate::v2::generated::BannedPeer> for &types::network::BannedPeer {
fn into_request(self) -> tonic::Request<crate::v2::generated::BannedPeer> {
tonic::Request::new(crate::v2::generated::BannedPeer {
ip_address: Some(crate::v2::generated::IpAddress {
value: self.0.to_string(),
}),
})
}
}
impl From<generated::PeerId> for types::network::PeerId {
fn from(value: generated::PeerId) -> Self {
types::network::PeerId(value.value)
}
}
impl TryFrom<generated::PeersInfo> for types::network::PeersInfo {
type Error = anyhow::Error;
fn try_from(peers_info: generated::PeersInfo) -> Result<Self, Self::Error> {
let peers = peers_info
.peers
.into_iter()
.map(|peer| {
let peer_consensus_info =
Upward::from(peer.consensus_info).and_then(|info| match info {
generated::peers_info::peer::ConsensusInfo::Bootstrapper(_) => {
Upward::Known(types::network::PeerConsensusInfo::Bootstrapper)
}
generated::peers_info::peer::ConsensusInfo::NodeCatchupStatus(status) => {
let Upward::Known(status) = Upward::from(
generated::peers_info::peer::CatchupStatus::try_from(status).ok(),
) else {
return Upward::Known(types::network::PeerConsensusInfo::Node(
Upward::Unknown(()),
));
};
let status = match status {
generated::peers_info::peer::CatchupStatus::Uptodate => {
types::network::PeerCatchupStatus::UpToDate
}
generated::peers_info::peer::CatchupStatus::Pending => {
types::network::PeerCatchupStatus::Pending
}
generated::peers_info::peer::CatchupStatus::Catchingup => {
types::network::PeerCatchupStatus::CatchingUp
}
};
Upward::Known(types::network::PeerConsensusInfo::Node(Upward::Known(
status,
)))
}
});
let stats = peer.network_stats.require()?;
let network_stats = types::network::NetworkStats {
packets_sent: stats.packets_sent,
packets_received: stats.packets_received,
latency: stats.latency,
};
Ok(types::network::Peer {
peer_id: peer.peer_id.require()?.into(),
consensus_info: peer_consensus_info,
network_stats,
addr: peer.socket_address.require()?.try_into()?,
})
})
.collect::<anyhow::Result<Vec<types::network::Peer>>>()?;
Ok(types::network::PeersInfo { peers })
}
}
impl TryFrom<generated::node_info::NetworkInfo> for types::NetworkInfo {
type Error = anyhow::Error;
fn try_from(network_info: generated::node_info::NetworkInfo) -> Result<Self, Self::Error> {
Ok(types::NetworkInfo {
node_id: network_info.node_id.require()?.value,
peer_total_sent: network_info.peer_total_sent,
peer_total_received: network_info.peer_total_received,
avg_bps_in: network_info.avg_bps_in,
avg_bps_out: network_info.avg_bps_out,
})
}
}
impl IntoRequest<crate::v2::generated::PeerToBan> for types::network::PeerToBan {
fn into_request(self) -> tonic::Request<crate::v2::generated::PeerToBan> {
tonic::Request::new(match self {
types::network::PeerToBan::IpAddr(ip_addr) => crate::v2::generated::PeerToBan {
ip_address: Some(crate::v2::generated::IpAddress {
value: ip_addr.to_string(),
}),
},
})
}
}
impl TryFrom<generated::node_info::Details> for types::NodeDetails {
type Error = anyhow::Error;
fn try_from(details: generated::node_info::Details) -> Result<Self, Self::Error> {
match details {
generated::node_info::Details::Bootstrapper(_) => Ok(types::NodeDetails::Bootstrapper),
generated::node_info::Details::Node(status) => {
let Upward::Known(consensus_status) = Upward::from(status.consensus_status) else {
return Ok(types::NodeDetails::Node(Upward::Unknown(())));
};
let consensus_status = match consensus_status {
generated::node_info::node::ConsensusStatus::NotRunning(_) => {
types::NodeConsensusStatus::ConsensusNotRunning
}
generated::node_info::node::ConsensusStatus::Passive(_) => {
types::NodeConsensusStatus::ConsensusPassive
}
generated::node_info::node::ConsensusStatus::Active(baker) => {
let baker_id = baker.baker_id.require()?.into();
let Upward::Known(status) = Upward::from(baker.status) else {
return Ok(types::NodeDetails::Node(Upward::Unknown(())));
};
match status {
generated::node_info::baker_consensus_info::Status::PassiveCommitteeInfo(0) => types::NodeConsensusStatus::NotInCommittee(baker_id),
generated::node_info::baker_consensus_info::Status::PassiveCommitteeInfo(1) => types::NodeConsensusStatus::AddedButNotActiveInCommittee(baker_id),
generated::node_info::baker_consensus_info::Status::PassiveCommitteeInfo(2) => types::NodeConsensusStatus::AddedButWrongKeys(baker_id),
generated::node_info::baker_consensus_info::Status::ActiveBakerCommitteeInfo(_) => types::NodeConsensusStatus::Baker(baker_id),
generated::node_info::baker_consensus_info::Status::ActiveFinalizerCommitteeInfo(_) => types::NodeConsensusStatus::Finalizer(baker_id),
_ => anyhow::bail!("Malformed baker status")
}
}
};
Ok(types::NodeDetails::Node(Upward::Known(consensus_status)))
}
}
}
}
impl TryFrom<generated::NodeInfo> for types::NodeInfo {
type Error = anyhow::Error;
fn try_from(node_info: generated::NodeInfo) -> Result<Self, Self::Error> {
let version = semver::Version::parse(&node_info.peer_version)?;
let local_time = chrono::DateTime::<chrono::Utc>::from(std::time::UNIX_EPOCH)
+ chrono::TimeDelta::try_milliseconds(node_info.local_time.require()?.value as i64)
.context("Node local time out of bounds!")?;
let uptime = chrono::Duration::try_from(types::DurationSeconds::from(
node_info.peer_uptime.require()?.value,
))?;
let network_info = node_info.network_info.require()?.try_into()?;
let details = Upward::from(node_info.details)
.map(types::NodeDetails::try_from)
.transpose()?;
Ok(types::NodeInfo {
version,
local_time,
uptime,
network_info,
details,
})
}
}
pub trait IntoBlockIdentifier {
fn into_block_identifier(self) -> BlockIdentifier;
}
impl IntoBlockIdentifier for BlockIdentifier {
fn into_block_identifier(self) -> BlockIdentifier {
self
}
}
impl<X: IntoBlockIdentifier + Copy> IntoBlockIdentifier for &X {
fn into_block_identifier(self) -> BlockIdentifier {
(*self).into_block_identifier()
}
}
impl IntoBlockIdentifier for BlockHash {
fn into_block_identifier(self) -> BlockIdentifier {
BlockIdentifier::Given(self)
}
}
impl IntoBlockIdentifier for AbsoluteBlockHeight {
fn into_block_identifier(self) -> BlockIdentifier {
BlockIdentifier::AbsoluteHeight(self)
}
}
impl IntoBlockIdentifier for RelativeBlockHeight {
fn into_block_identifier(self) -> BlockIdentifier {
BlockIdentifier::RelativeHeight(self)
}
}
impl Client {
pub async fn new<E>(endpoint: E) -> Result<Self, tonic::transport::Error>
where
E: TryInto<tonic::transport::Endpoint>,
E::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let client = generated::queries_client::QueriesClient::connect(endpoint).await?;
Ok(Self { client })
}
pub async fn get_account_info(
&mut self,
acc: &AccountIdentifier,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<AccountInfo>> {
let response = self
.client
.get_account_info((acc, &bi.into_block_identifier()))
.await?;
let block_hash = extract_metadata(&response)?;
let response = AccountInfo::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_next_account_sequence_number(
&mut self,
account_address: &AccountAddress,
) -> endpoints::QueryResult<types::queries::AccountNonceResponse> {
let response = self
.client
.get_next_account_sequence_number(account_address)
.await?;
let response = types::queries::AccountNonceResponse::try_from(response.into_inner())?;
Ok(response)
}
pub async fn get_consensus_info(
&mut self,
) -> endpoints::QueryResult<types::queries::ConsensusInfo> {
let response = self
.client
.get_consensus_info(generated::Empty::default())
.await?;
let response = types::queries::ConsensusInfo::try_from(response.into_inner())?;
Ok(response)
}
pub async fn get_cryptographic_parameters(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::CryptographicParameters>> {
let response = self
.client
.get_cryptographic_parameters(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::CryptographicParameters::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_account_list(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<AccountAddress, tonic::Status>>>,
> {
let response = self
.client
.get_account_list(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| x.and_then(TryFrom::try_from));
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_module_list(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<ModuleReference, tonic::Status>>>,
> {
let response = self
.client
.get_module_list(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| x.and_then(TryFrom::try_from));
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_module_source(
&mut self,
module_ref: &ModuleReference,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::smart_contracts::WasmModule>> {
let response = self
.client
.get_module_source((module_ref, &bi.into_block_identifier()))
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::smart_contracts::WasmModule::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_instance_list(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<ContractAddress, tonic::Status>>>,
> {
let response = self
.client
.get_instance_list(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| x.map(From::from));
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_instance_info(
&mut self,
address: ContractAddress,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<InstanceInfo>> {
let response = self
.client
.get_instance_info((address, &bi.into_block_identifier()))
.await?;
let block_hash = extract_metadata(&response)?;
let response = InstanceInfo::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_ancestors(
&mut self,
bi: impl IntoBlockIdentifier,
limit: u64,
) -> endpoints::QueryResult<QueryResponse<impl Stream<Item = Result<BlockHash, tonic::Status>>>>
{
let response = self
.client
.get_ancestors((&bi.into_block_identifier(), limit))
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| x.and_then(TryFrom::try_from));
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_finalized_blocks(
&mut self,
) -> endpoints::QueryResult<impl Stream<Item = Result<FinalizedBlockInfo, tonic::Status>>> {
let response = self
.client
.get_finalized_blocks(generated::Empty::default())
.await?;
let stream = response.into_inner().map(|x| match x {
Ok(v) => {
let block_hash = v.hash.require().and_then(TryFrom::try_from)?;
let height = v.height.require()?.into();
Ok(FinalizedBlockInfo { block_hash, height })
}
Err(x) => Err(x),
});
Ok(stream)
}
pub async fn get_instance_state(
&mut self,
ca: ContractAddress,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<(Vec<u8>, Vec<u8>), tonic::Status>>>,
> {
let response = self
.client
.get_instance_state((ca, &bi.into_block_identifier()))
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| match x {
Ok(v) => {
let key = v.key;
let value = v.value;
Ok((key, value))
}
Err(x) => Err(x),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn instance_state_lookup(
&mut self,
ca: ContractAddress,
key: impl Into<Vec<u8>>,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<Vec<u8>>> {
let response = self
.client
.instance_state_lookup((ca, &bi.into_block_identifier(), key))
.await?;
let block_hash = extract_metadata(&response)?;
Ok(QueryResponse {
block_hash,
response: response.into_inner().value,
})
}
pub async fn get_block_item_status(
&mut self,
th: &TransactionHash,
) -> endpoints::QueryResult<TransactionStatus> {
let response = self.client.get_block_item_status(th).await?;
let response = TransactionStatus::try_from(response.into_inner())?;
Ok(response)
}
pub async fn send_block_item<P: PayloadLike>(
&mut self,
bi: &transactions::BlockItem<P>,
) -> endpoints::RPCResult<TransactionHash> {
let response = self.client.send_block_item(bi).await?;
let response = TransactionHash::try_from(response.into_inner())?;
Ok(response)
}
pub async fn send_account_transaction<P: PayloadLike>(
&mut self,
at: transactions::AccountTransaction<P>,
) -> endpoints::RPCResult<TransactionHash> {
self.send_block_item(&at.into()).await
}
pub async fn get_account_transaction_sign_hash(
&mut self,
header: &transactions::TransactionHeader,
payload: &transactions::Payload,
) -> endpoints::RPCResult<TransactionSignHash> {
let response = self
.client
.get_account_transaction_sign_hash((header, payload))
.await?;
let response = TransactionSignHash::try_from(response.into_inner())?;
Ok(response)
}
pub async fn wait_until_finalized(
&mut self,
hash: &types::hashes::TransactionHash,
) -> endpoints::QueryResult<(types::hashes::BlockHash, types::BlockItemSummary)> {
let hash = *hash;
let process_response = |response| {
if let types::TransactionStatus::Finalized(blocks) = response {
let mut iter = blocks.into_iter();
if let Some(rv) = iter.next() {
if iter.next().is_some() {
Err(tonic::Status::internal(
"Finalized transaction finalized into multiple blocks. This cannot \
happen.",
)
.into())
} else {
Ok::<_, QueryError>(Some(rv))
}
} else {
Err(tonic::Status::internal(
"Finalized transaction finalized into no blocks. This cannot happen.",
)
.into())
}
} else {
Ok(None)
}
};
match process_response(self.get_block_item_status(&hash).await?)? {
Some(rv) => Ok(rv),
None => {
let mut blocks = self.get_finalized_blocks().await?;
while blocks.next().await.transpose()?.is_some() {
if let Some(rv) = process_response(self.get_block_item_status(&hash).await?)? {
return Ok(rv);
}
}
Err(QueryError::NotFound)
}
}
}
pub async fn invoke_instance(
&mut self,
bi: impl IntoBlockIdentifier,
context: &ContractContext,
) -> endpoints::QueryResult<QueryResponse<InvokeContractResult>> {
let response = self
.client
.invoke_instance((&bi.into_block_identifier(), context))
.await?;
let block_hash = extract_metadata(&response)?;
let response = InvokeContractResult::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn begin_dry_run(&mut self) -> endpoints::QueryResult<dry_run::DryRun> {
Ok(dry_run::DryRun::new(&mut self.client).await?)
}
pub async fn dry_run(
&mut self,
bi: impl IntoBlockIdentifier,
) -> dry_run::DryRunResult<(dry_run::DryRun, dry_run::BlockStateLoaded)> {
let mut runner = dry_run::DryRun::new(&mut self.client).await?;
let load_result = runner.load_block_state(bi).await?;
Ok(WithRemainingQuota {
inner: (runner, load_result.inner),
quota_remaining: load_result.quota_remaining,
})
}
pub async fn get_block_info(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::queries::BlockInfo>> {
let response = self
.client
.get_block_info(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::queries::BlockInfo::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn is_payday_block(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<bool>> {
let mut special_events = self.get_block_special_events(bi).await?;
let block_hash = special_events.block_hash;
while let Some(event) = special_events.response.next().await.transpose()? {
let Upward::Known(event) = event else {
continue;
};
let has_payday_event = matches!(
event,
SpecialTransactionOutcome::PaydayPoolReward { .. }
| SpecialTransactionOutcome::PaydayAccountReward { .. }
| SpecialTransactionOutcome::PaydayFoundationReward { .. }
);
if has_payday_event {
return Ok(QueryResponse {
block_hash,
response: true,
});
};
}
Ok(QueryResponse {
block_hash,
response: false,
})
}
pub async fn get_baker_list(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::BakerId, tonic::Status>>>,
> {
let response = self
.client
.get_baker_list(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|x| x.map(From::from));
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_pool_info(
&mut self,
block_id: impl IntoBlockIdentifier,
baker_id: types::BakerId,
) -> endpoints::QueryResult<QueryResponse<types::BakerPoolStatus>> {
let response = self
.client
.get_pool_info((&block_id.into_block_identifier(), baker_id))
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::BakerPoolStatus::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_passive_delegation_info(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::PassiveDelegationStatus>> {
let response = self
.client
.get_passive_delegation_info(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::PassiveDelegationStatus::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_blocks_at_height(
&mut self,
blocks_at_height_input: &endpoints::BlocksAtHeightInput,
) -> endpoints::QueryResult<Vec<BlockHash>> {
let response = self
.client
.get_blocks_at_height(blocks_at_height_input)
.await?;
let blocks = response
.into_inner()
.blocks
.into_iter()
.map(TryFrom::try_from)
.collect::<Result<_, tonic::Status>>()?;
Ok(blocks)
}
pub async fn get_tokenomics_info(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::RewardsOverview>> {
let response = self
.client
.get_tokenomics_info(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::RewardsOverview::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_pool_delegators(
&mut self,
bi: impl IntoBlockIdentifier,
baker_id: types::BakerId,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::DelegatorInfo, tonic::Status>>>,
> {
let response = self
.client
.get_pool_delegators((&bi.into_block_identifier(), baker_id))
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(delegator) => delegator.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_pool_delegators_reward_period(
&mut self,
bi: impl IntoBlockIdentifier,
baker_id: types::BakerId,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::DelegatorRewardPeriodInfo, tonic::Status>>>,
> {
let response = self
.client
.get_pool_delegators_reward_period((&bi.into_block_identifier(), baker_id))
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(delegator) => delegator.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_passive_delegators(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::DelegatorInfo, tonic::Status>>>,
> {
let response = self
.client
.get_passive_delegators(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(delegator) => delegator.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_passive_delegators_reward_period(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::DelegatorRewardPeriodInfo, tonic::Status>>>,
> {
let response = self
.client
.get_passive_delegators_reward_period(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(delegator) => delegator.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_branches(&mut self) -> endpoints::QueryResult<types::queries::Branch> {
let response = self
.client
.get_branches(generated::Empty::default())
.await?;
let response = types::queries::Branch::try_from(response.into_inner())?;
Ok(response)
}
pub async fn get_election_info(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::BirkParameters>> {
let response = self
.client
.get_election_info(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::BirkParameters::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_identity_providers(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<
impl Stream<
Item = Result<
crate::id::types::IpInfo<crate::id::constants::IpPairing>,
tonic::Status,
>,
>,
>,
> {
let response = self
.client
.get_identity_providers(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(ip_info) => ip_info.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_anonymity_revokers(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<
impl Stream<
Item = Result<
crate::id::types::ArInfo<crate::id::constants::ArCurve>,
tonic::Status,
>,
>,
>,
> {
let response = self
.client
.get_anonymity_revokers(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(ar_info) => ar_info.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_account_non_finalized_transactions(
&mut self,
account_address: &AccountAddress,
) -> endpoints::QueryResult<impl Stream<Item = Result<TransactionHash, tonic::Status>>> {
let response = self
.client
.get_account_non_finalized_transactions(account_address)
.await?;
let stream = response.into_inner().map(|result| match result {
Ok(transaction_hash) => transaction_hash.try_into(),
Err(err) => Err(err),
});
Ok(stream)
}
pub async fn get_block_items(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<Upward<BlockItem<EncodedPayload>>, tonic::Status>>>,
> {
let response = self
.client
.get_block_items(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(summary) => summary.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_finalized_block_item(
&mut self,
th: TransactionHash,
) -> endpoints::QueryResult<(
Upward<BlockItem<EncodedPayload>>,
BlockHash,
BlockItemSummary,
)> {
let status = self.get_block_item_status(&th).await?;
let Some((bh, status)) = status.is_finalized() else {
return Err(QueryError::NotFound);
};
let mut response = self
.client
.get_block_items(&bh.into_block_identifier())
.await?
.into_inner();
while let Some(tx) = response.try_next().await? {
let tx_hash = TransactionHash::try_from(tx.hash.clone().require()?)?;
if tx_hash == th {
return Ok((tx.try_into()?, *bh, status.clone()));
}
}
Err(endpoints::QueryError::NotFound)
}
pub async fn shutdown(&mut self) -> endpoints::RPCResult<()> {
self.client.shutdown(generated::Empty::default()).await?;
Ok(())
}
pub async fn peer_connect(&mut self, addr: std::net::SocketAddr) -> endpoints::RPCResult<()> {
let peer_connection = generated::IpSocketAddress {
ip: Some(generated::IpAddress {
value: addr.ip().to_string(),
}),
port: Some(generated::Port {
value: addr.port() as u32,
}),
};
self.client.peer_connect(peer_connection).await?;
Ok(())
}
pub async fn peer_disconnect(
&mut self,
addr: std::net::SocketAddr,
) -> endpoints::RPCResult<()> {
let peer_connection = generated::IpSocketAddress {
ip: Some(generated::IpAddress {
value: addr.ip().to_string(),
}),
port: Some(generated::Port {
value: addr.port() as u32,
}),
};
self.client.peer_disconnect(peer_connection).await?;
Ok(())
}
pub async fn get_banned_peers(
&mut self,
) -> endpoints::RPCResult<Vec<super::types::network::BannedPeer>> {
Ok(self
.client
.get_banned_peers(generated::Empty::default())
.await?
.into_inner()
.peers
.into_iter()
.map(super::types::network::BannedPeer::try_from)
.collect::<anyhow::Result<Vec<super::types::network::BannedPeer>>>()?)
}
pub async fn ban_peer(
&mut self,
peer_to_ban: super::types::network::PeerToBan,
) -> endpoints::RPCResult<()> {
self.client.ban_peer(peer_to_ban).await?;
Ok(())
}
pub async fn unban_peer(
&mut self,
banned_peer: &super::types::network::BannedPeer,
) -> endpoints::RPCResult<()> {
self.client.unban_peer(banned_peer).await?;
Ok(())
}
pub async fn dump_start(
&mut self,
file: &std::path::Path,
raw: bool,
) -> endpoints::RPCResult<()> {
let file_str = file.to_str().ok_or_else(|| {
tonic::Status::invalid_argument(
"The provided path cannot is not a valid UTF8 string, so cannot be used.",
)
})?;
self.client
.dump_start(generated::DumpRequest {
file: file_str.to_string(),
raw,
})
.await?;
Ok(())
}
pub async fn dump_stop(&mut self) -> endpoints::RPCResult<()> {
self.client.dump_stop(generated::Empty::default()).await?;
Ok(())
}
pub async fn get_peers_info(&mut self) -> endpoints::RPCResult<types::network::PeersInfo> {
let response = self
.client
.get_peers_info(generated::Empty::default())
.await?;
let peers_info = types::network::PeersInfo::try_from(response.into_inner())?;
Ok(peers_info)
}
pub async fn get_node_info(&mut self) -> endpoints::RPCResult<types::NodeInfo> {
let response = self
.client
.get_node_info(generated::Empty::default())
.await?;
let node_info = types::NodeInfo::try_from(response.into_inner())?;
Ok(node_info)
}
pub async fn get_baker_earliest_win_time(
&mut self,
bid: types::BakerId,
) -> endpoints::RPCResult<chrono::DateTime<chrono::Utc>> {
let ts = self.client.get_baker_earliest_win_time(bid).await?;
let local_time = ts.into_inner().try_into()?;
Ok(local_time)
}
pub async fn get_block_transaction_events(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::BlockItemSummary, tonic::Status>>>,
> {
let response = self
.client
.get_block_transaction_events(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(summary) => summary.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_block_special_events(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<
impl Stream<Item = Result<Upward<types::SpecialTransactionOutcome>, tonic::Status>>,
>,
> {
let response = self
.client
.get_block_special_events(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(summary) => summary.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_block_pending_updates(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::queries::PendingUpdate, tonic::Status>>>,
> {
let response = self
.client
.get_block_pending_updates(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(update) => update.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_winning_bakers_epoch(
&mut self,
ei: impl Into<EpochIdentifier>,
) -> endpoints::QueryResult<impl Stream<Item = Result<types::WinningBaker, tonic::Status>>>
{
let response = self.client.get_winning_bakers_epoch(&ei.into()).await?;
let stream = response.into_inner().map(|result| match result {
Ok(wb) => wb.try_into(),
Err(err) => Err(err),
});
Ok(stream)
}
pub async fn get_first_block_epoch(
&mut self,
ei: impl Into<EpochIdentifier>,
) -> endpoints::QueryResult<BlockHash> {
let response = self.client.get_first_block_epoch(&ei.into()).await?;
Ok(response.into_inner().try_into()?)
}
pub async fn get_consensus_detailed_status(
&mut self,
genesis_index: Option<GenesisIndex>,
) -> endpoints::RPCResult<ConsensusDetailedStatus> {
let query = generated::ConsensusDetailedStatusQuery {
genesis_index: genesis_index.map(Into::into),
};
let response = self.client.get_consensus_detailed_status(query).await?;
Ok(response.into_inner().try_into()?)
}
pub async fn get_next_update_sequence_numbers(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<types::queries::NextUpdateSequenceNumbers>> {
let response = self
.client
.get_next_update_sequence_numbers(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = types::queries::NextUpdateSequenceNumbers::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_scheduled_release_accounts(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<AccountPending, tonic::Status>>>,
> {
let response = self
.client
.get_scheduled_release_accounts(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(pending) => pending.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_cooldown_accounts(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<AccountPending, tonic::Status>>>,
> {
let response = self
.client
.get_cooldown_accounts(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(pending) => pending.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_pre_cooldown_accounts(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<AccountIndex, tonic::Status>>>,
> {
let response = self
.client
.get_pre_cooldown_accounts(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(account) => Ok(account.into()),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_pre_pre_cooldown_accounts(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<AccountIndex, tonic::Status>>>,
> {
let response = self
.client
.get_pre_pre_cooldown_accounts(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(account) => Ok(account.into()),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_block_chain_parameters(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<ChainParameters>> {
let response = self
.client
.get_block_chain_parameters(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = ChainParameters::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_block_certificates(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<block_certificates::BlockCertificates>> {
let response = self
.client
.get_block_certificates(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = block_certificates::BlockCertificates::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_block_finalization_summary(
&mut self,
block_id: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<Option<types::FinalizationSummary>>> {
let response = self
.client
.get_block_finalization_summary(&block_id.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let response = response.into_inner().try_into()?;
Ok(QueryResponse {
block_hash,
response,
})
}
pub async fn get_finalized_blocks_from(
&mut self,
start_height: AbsoluteBlockHeight,
) -> endpoints::QueryResult<FinalizedBlocksStream> {
let mut fin_height = self.get_consensus_info().await?.last_finalized_block_height;
let (sender, receiver) = tokio::sync::mpsc::channel(100);
let mut client = self.clone();
let handle = tokio::spawn(async move {
let mut height = start_height;
loop {
if height > fin_height {
fin_height = client
.get_consensus_info()
.await?
.last_finalized_block_height;
if height > fin_height {
break;
}
} else {
let mut bi = client.get_blocks_at_height(&height.into()).await?;
let block_hash = bi.pop().ok_or(endpoints::QueryError::NotFound)?;
let info = FinalizedBlockInfo { block_hash, height };
if sender.send(info).await.is_err() {
return Ok(());
}
height = height.next();
}
}
let mut stream = client.get_finalized_blocks().await?;
while let Some(fbi) = stream.next().await.transpose()? {
while height < fbi.height {
let mut bi = client.get_blocks_at_height(&height.into()).await?;
let block_hash = bi.pop().ok_or(endpoints::QueryError::NotFound)?;
let info = FinalizedBlockInfo { block_hash, height };
if sender.send(info).await.is_err() {
return Ok(());
}
height = height.next();
}
if sender.send(fbi).await.is_err() {
return Ok(());
}
height = height.next();
}
Ok(())
});
Ok(FinalizedBlocksStream { handle, receiver })
}
pub async fn find_account_creation(
&mut self,
range: impl std::ops::RangeBounds<AbsoluteBlockHeight>,
addr: AccountAddress,
) -> QueryResult<(AbsoluteBlockHeight, BlockHash, AccountInfo)> {
self.find_at_lowest_height(range, |mut client, height| async move {
match client.get_account_info(&addr.into(), &height).await {
Ok(ii) => Ok(Some((height, ii.block_hash, ii.response))),
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
})
.await
}
pub async fn find_instance_creation(
&mut self,
range: impl std::ops::RangeBounds<AbsoluteBlockHeight>,
addr: ContractAddress,
) -> QueryResult<(AbsoluteBlockHeight, BlockHash, InstanceInfo)> {
self.find_at_lowest_height(range, |mut client, height| async move {
match client.get_instance_info(addr, &height).await {
Ok(ii) => Ok(Some((height, ii.block_hash, ii.response))),
Err(e) if e.is_not_found() => Ok(None),
Err(e) => Err(e),
}
})
.await
}
pub async fn find_first_finalized_block_no_earlier_than(
&mut self,
range: impl std::ops::RangeBounds<AbsoluteBlockHeight>,
time: chrono::DateTime<chrono::Utc>,
) -> QueryResult<types::queries::BlockInfo> {
self.find_at_lowest_height(range, move |mut client, height| async move {
let info = client.get_block_info(&height).await?.response;
if info.block_slot_time >= time {
Ok(Some(info))
} else {
Ok(None)
}
})
.await
}
pub async fn find_at_lowest_height<A, F: futures::Future<Output = QueryResult<Option<A>>>>(
&mut self,
range: impl std::ops::RangeBounds<AbsoluteBlockHeight>,
test: impl Fn(Self, AbsoluteBlockHeight) -> F,
) -> QueryResult<A> {
let mut start = match range.start_bound() {
std::ops::Bound::Included(s) => u64::from(*s),
std::ops::Bound::Excluded(e) => u64::from(*e).saturating_add(1),
std::ops::Bound::Unbounded => 0,
};
let mut end = {
let ci = self.get_consensus_info().await?;
let bound = |end: u64| std::cmp::min(end, ci.last_finalized_block_height.into());
match range.end_bound() {
std::ops::Bound::Included(e) => bound(u64::from(*e)),
std::ops::Bound::Excluded(e) => {
bound(u64::from(*e).checked_sub(1).ok_or(QueryError::NotFound)?)
}
std::ops::Bound::Unbounded => u64::from(ci.last_finalized_block_height),
}
};
if end < start {
return Err(QueryError::NotFound);
}
let mut last_found = None;
while start < end {
let mid = start + (end - start) / 2;
let ok = test(self.clone(), mid.into()).await?;
if ok.is_some() {
end = mid;
last_found = ok;
} else {
start = mid + 1;
}
}
last_found.ok_or(QueryError::NotFound)
}
#[deprecated(note = "Use [`find_at_lowest_height`](./struct.Client.html#method.\
find_at_lowest_height) instead since it avoids an extra call.")]
pub async fn find_earliest_finalized<A, F: futures::Future<Output = QueryResult<Option<A>>>>(
&mut self,
range: impl std::ops::RangeBounds<AbsoluteBlockHeight>,
test: impl Fn(Self, AbsoluteBlockHeight, BlockHash) -> F,
) -> QueryResult<A> {
let mut start = match range.start_bound() {
std::ops::Bound::Included(s) => u64::from(*s),
std::ops::Bound::Excluded(e) => u64::from(*e).saturating_add(1),
std::ops::Bound::Unbounded => 0,
};
let mut end = {
let ci = self.get_consensus_info().await?;
let bound = |end: u64| std::cmp::min(end, ci.last_finalized_block_height.into());
match range.end_bound() {
std::ops::Bound::Included(e) => bound(u64::from(*e)),
std::ops::Bound::Excluded(e) => {
bound(u64::from(*e).checked_sub(1).ok_or(QueryError::NotFound)?)
}
std::ops::Bound::Unbounded => u64::from(ci.last_finalized_block_height),
}
};
if end < start {
return Err(QueryError::NotFound);
}
let mut last_found = None;
while start < end {
let mid = start + (end - start) / 2;
let bh = self
.get_blocks_at_height(&AbsoluteBlockHeight::from(mid).into())
.await?[0]; let ok = test(self.clone(), mid.into(), bh).await?;
if ok.is_some() {
end = mid;
last_found = ok;
} else {
start = mid + 1;
}
}
last_found.ok_or(QueryError::NotFound)
}
pub async fn get_bakers_reward_period(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<types::BakerRewardPeriodInfo, tonic::Status>>>,
> {
let response = self
.client
.get_bakers_reward_period(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(baker) => baker.try_into(),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_token_list(
&mut self,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<
QueryResponse<impl Stream<Item = Result<protocol_level_tokens::TokenId, tonic::Status>>>,
> {
let response = self
.client
.get_token_list(&bi.into_block_identifier())
.await?;
let block_hash = extract_metadata(&response)?;
let stream = response.into_inner().map(|result| match result {
Ok(token_id) => protocol_level_tokens::TokenId::try_from(token_id),
Err(err) => Err(err),
});
Ok(QueryResponse {
block_hash,
response: stream,
})
}
pub async fn get_token_info(
&mut self,
token_id: protocol_level_tokens::TokenId,
bi: impl IntoBlockIdentifier,
) -> endpoints::QueryResult<QueryResponse<protocol_level_tokens::TokenInfo>> {
let request = generated::TokenInfoRequest {
block_hash: Some((&bi.into_block_identifier()).into()),
token_id: Some(token_id.into()),
};
let response = self.client.get_token_info(request).await?;
let block_hash = extract_metadata(&response)?;
let response = protocol_level_tokens::TokenInfo::try_from(response.into_inner())?;
Ok(QueryResponse {
block_hash,
response,
})
}
}
pub struct FinalizedBlocksStream {
handle: tokio::task::JoinHandle<endpoints::QueryResult<()>>,
receiver: tokio::sync::mpsc::Receiver<FinalizedBlockInfo>,
}
impl Drop for FinalizedBlocksStream {
fn drop(&mut self) {
self.handle.abort();
}
}
impl FinalizedBlocksStream {
pub async fn next(&mut self) -> Option<FinalizedBlockInfo> {
self.receiver.recv().await
}
pub async fn next_timeout(
&mut self,
duration: std::time::Duration,
) -> Result<Option<FinalizedBlockInfo>, tokio::time::error::Elapsed> {
tokio::time::timeout(duration, async move { self.next().await }).await
}
pub async fn next_chunk(
&mut self,
n: usize,
) -> Result<Vec<FinalizedBlockInfo>, Vec<FinalizedBlockInfo>> {
let mut out = Vec::with_capacity(n);
let first = self.receiver.recv().await;
match first {
Some(v) => out.push(v),
None => {
return Err(out);
}
}
for _ in 1..n {
match self.receiver.try_recv() {
Ok(v) => {
out.push(v);
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
break;
}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return Err(out),
}
}
Ok(out)
}
pub async fn next_chunk_timeout(
&mut self,
n: usize,
duration: std::time::Duration,
) -> Result<(bool, Vec<FinalizedBlockInfo>), tokio::time::error::Elapsed> {
let mut out = Vec::with_capacity(n);
let first = self.next_timeout(duration).await?;
match first {
Some(v) => out.push(v),
None => return Ok((true, out)),
}
for _ in 1..n {
match self.receiver.try_recv() {
Ok(v) => {
out.push(v);
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
break;
}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
return Ok((true, out))
}
}
}
Ok((false, out))
}
}
fn extract_metadata<T>(response: &tonic::Response<T>) -> endpoints::RPCResult<BlockHash> {
match response.metadata().get("blockhash") {
Some(bytes) => {
let bytes = bytes.as_bytes();
if bytes.len() == 64 {
let mut hash = [0u8; 32];
if hex::decode_to_slice(bytes, &mut hash).is_err() {
tonic::Status::unknown("Response does correctly encode the block hash.");
}
Ok(hash.into())
} else {
Err(endpoints::RPCError::CallError(tonic::Status::unknown(
"Response does not include the expected metadata.",
)))
}
}
None => Err(endpoints::RPCError::CallError(tonic::Status::unknown(
"Response does not include the expected metadata.",
))),
}
}
pub(crate) trait Require<E> {
type A;
fn require(self) -> Result<Self::A, E>;
}
impl<A> Require<tonic::Status> for Option<A> {
type A = A;
fn require(self) -> Result<Self::A, tonic::Status> {
match self {
Some(v) => Ok(v),
None => Err(tonic::Status::invalid_argument("missing field in response")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_ident_from_str() -> anyhow::Result<()> {
let b1 = "best".parse::<BlockIdentifier>()?;
assert_eq!(b1, BlockIdentifier::Best);
let b2 = "lastFinal".parse::<BlockIdentifier>()?;
assert_eq!(b2, BlockIdentifier::LastFinal);
let b3 = "lastfinal".parse::<BlockIdentifier>()?;
assert_eq!(b3, BlockIdentifier::LastFinal);
let b4 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
.parse::<BlockIdentifier>()?;
assert_eq!(
b4,
BlockIdentifier::Given(
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".parse()?
)
);
let b5 = "@33".parse::<BlockIdentifier>()?;
assert_eq!(b5, BlockIdentifier::AbsoluteHeight(33.into()));
let b6 = "@33/3".parse::<BlockIdentifier>()?;
assert_eq!(
b6,
BlockIdentifier::RelativeHeight(RelativeBlockHeight {
genesis_index: 3.into(),
height: 33.into(),
restrict: false,
})
);
let b7 = "@33/3!".parse::<BlockIdentifier>()?;
assert_eq!(
b7,
BlockIdentifier::RelativeHeight(RelativeBlockHeight {
genesis_index: 3.into(),
height: 33.into(),
restrict: true,
})
);
Ok(())
}
}