mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-03 17:40:34 +00:00
Have monero-wallet use Transaction<Pruned>, not Transaction
This commit is contained in:
parent
7b8bcae396
commit
85fc31fd82
7 changed files with 115 additions and 107 deletions
|
@ -26,7 +26,7 @@ use serde_json::{Value, json};
|
||||||
|
|
||||||
use monero_serai::{
|
use monero_serai::{
|
||||||
io::*,
|
io::*,
|
||||||
transaction::{Input, Timelock, Transaction},
|
transaction::{Input, Timelock, Pruned, Transaction},
|
||||||
block::Block,
|
block::Block,
|
||||||
DEFAULT_LOCK_WINDOW,
|
DEFAULT_LOCK_WINDOW,
|
||||||
};
|
};
|
||||||
|
@ -361,14 +361,15 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, res)| {
|
.map(|(i, res)| {
|
||||||
let tx = Transaction::read::<&[u8]>(
|
let buf = rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?;
|
||||||
&mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?
|
let mut buf = buf.as_slice();
|
||||||
.as_ref(),
|
let tx = Transaction::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) {
|
||||||
)
|
|
||||||
.map_err(|_| match hash_hex(&res.tx_hash) {
|
|
||||||
Ok(hash) => RpcError::InvalidTransaction(hash),
|
Ok(hash) => RpcError::InvalidTransaction(hash),
|
||||||
Err(err) => err,
|
Err(err) => err,
|
||||||
})?;
|
})?;
|
||||||
|
if !buf.is_empty() {
|
||||||
|
Err(RpcError::InvalidNode("transaction had extra bytes after it".to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/monero-project/monero/issues/8311
|
// https://github.com/monero-project/monero/issues/8311
|
||||||
if res.as_hex.is_empty() {
|
if res.as_hex.is_empty() {
|
||||||
|
@ -391,6 +392,61 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the specified transactions in their pruned format.
|
||||||
|
async fn get_pruned_transactions(
|
||||||
|
&self,
|
||||||
|
hashes: &[[u8; 32]],
|
||||||
|
) -> Result<Vec<Transaction<Pruned>>, RpcError> {
|
||||||
|
if hashes.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
|
||||||
|
let mut all_txs = Vec::with_capacity(hashes.len());
|
||||||
|
while !hashes_hex.is_empty() {
|
||||||
|
// Monero errors if more than 100 is requested unless using a non-restricted RPC
|
||||||
|
// TODO: Cite
|
||||||
|
// TODO: Deduplicate with above
|
||||||
|
const TXS_PER_REQUEST: usize = 100;
|
||||||
|
let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
|
||||||
|
|
||||||
|
let txs: TransactionsResponse = self
|
||||||
|
.rpc_call(
|
||||||
|
"get_transactions",
|
||||||
|
Some(json!({
|
||||||
|
"txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
|
||||||
|
"prune": true,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !txs.missed_tx.is_empty() {
|
||||||
|
Err(RpcError::TransactionsNotFound(
|
||||||
|
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs.extend(txs.txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs
|
||||||
|
.iter()
|
||||||
|
.map(|res| {
|
||||||
|
let buf = rpc_hex(&res.pruned_as_hex)?;
|
||||||
|
let mut buf = buf.as_slice();
|
||||||
|
let tx =
|
||||||
|
Transaction::<Pruned>::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) {
|
||||||
|
Ok(hash) => RpcError::InvalidTransaction(hash),
|
||||||
|
Err(err) => err,
|
||||||
|
})?;
|
||||||
|
if !buf.is_empty() {
|
||||||
|
Err(RpcError::InvalidNode("pruned transaction had extra bytes after it".to_string()))?;
|
||||||
|
}
|
||||||
|
Ok(tx)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the specified transaction.
|
/// Get the specified transaction.
|
||||||
///
|
///
|
||||||
/// The received transaction will be hashed in order to verify the correct transaction was
|
/// The received transaction will be hashed in order to verify the correct transaction was
|
||||||
|
@ -399,6 +455,11 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||||
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the specified transaction in its pruned format.
|
||||||
|
async fn get_pruned_transaction(&self, tx: [u8; 32]) -> Result<Transaction<Pruned>, RpcError> {
|
||||||
|
self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the hash of a block from the node.
|
/// Get the hash of a block from the node.
|
||||||
///
|
///
|
||||||
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
|
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
|
||||||
|
@ -469,35 +530,6 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the transactions within a block.
|
|
||||||
///
|
|
||||||
/// This function returns all transactions in the block, including the miner's transaction.
|
|
||||||
///
|
|
||||||
/// This function does not verify the returned transactions are the ones committed to by the
|
|
||||||
/// block's header.
|
|
||||||
async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
|
|
||||||
let block = self.get_block(hash).await?;
|
|
||||||
let mut res = vec![block.miner_transaction];
|
|
||||||
res.extend(self.get_transactions(&block.transactions).await?);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the transactions within a block.
|
|
||||||
///
|
|
||||||
/// This function returns all transactions in the block, including the miner's transaction.
|
|
||||||
///
|
|
||||||
/// This function does not verify the returned transactions are the ones committed to by the
|
|
||||||
/// block's header.
|
|
||||||
async fn get_block_transactions_by_number(
|
|
||||||
&self,
|
|
||||||
number: usize,
|
|
||||||
) -> Result<Vec<Transaction>, RpcError> {
|
|
||||||
let block = self.get_block_by_number(number).await?;
|
|
||||||
let mut res = vec![block.miner_transaction];
|
|
||||||
res.extend(self.get_transactions(&block.transactions).await?);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the currently estimated fee rate from the node.
|
/// Get the currently estimated fee rate from the node.
|
||||||
///
|
///
|
||||||
/// This may be manipulated to unsafe levels and MUST be sanity checked.
|
/// This may be manipulated to unsafe levels and MUST be sanity checked.
|
||||||
|
|
|
@ -57,31 +57,20 @@ async fn check_block(rpc: impl Rpc, block_i: usize) {
|
||||||
let txs_len = 1 + block.transactions.len();
|
let txs_len = 1 + block.transactions.len();
|
||||||
|
|
||||||
if !block.transactions.is_empty() {
|
if !block.transactions.is_empty() {
|
||||||
#[derive(Deserialize, Debug)]
|
// Test getting pruned transactions
|
||||||
struct TransactionResponse {
|
loop {
|
||||||
tx_hash: String,
|
match rpc.get_pruned_transactions(&block.transactions).await {
|
||||||
as_hex: String,
|
Ok(_) => break,
|
||||||
|
Err(RpcError::ConnectionError(e)) => {
|
||||||
|
println!("get_pruned_transactions ConnectionError: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => panic!("couldn't call get_pruned_transactions: {e:?}"),
|
||||||
}
|
}
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct TransactionsResponse {
|
|
||||||
#[serde(default)]
|
|
||||||
missed_tx: Vec<String>,
|
|
||||||
txs: Vec<TransactionResponse>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hashes_hex = block.transactions.iter().map(hex::encode).collect::<Vec<_>>();
|
let txs = loop {
|
||||||
let mut all_txs = vec![];
|
match rpc.get_transactions(&block.transactions).await {
|
||||||
while !hashes_hex.is_empty() {
|
|
||||||
let txs: TransactionsResponse = loop {
|
|
||||||
match rpc
|
|
||||||
.rpc_call(
|
|
||||||
"get_transactions",
|
|
||||||
Some(json!({
|
|
||||||
"txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::<Vec<_>>(),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(txs) => break txs,
|
Ok(txs) => break txs,
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
Err(RpcError::ConnectionError(e)) => {
|
||||||
println!("get_transactions ConnectionError: {e}");
|
println!("get_transactions ConnectionError: {e}");
|
||||||
|
@ -90,30 +79,9 @@ async fn check_block(rpc: impl Rpc, block_i: usize) {
|
||||||
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
|
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
assert!(txs.missed_tx.is_empty());
|
|
||||||
all_txs.extend(txs.txs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new();
|
let mut batch = BatchVerifier::new();
|
||||||
for (tx_hash, tx_res) in block.transactions.into_iter().zip(all_txs) {
|
for tx in txs {
|
||||||
assert_eq!(
|
|
||||||
tx_res.tx_hash,
|
|
||||||
hex::encode(tx_hash),
|
|
||||||
"node returned a transaction with different hash"
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx = Transaction::read(
|
|
||||||
&mut hex::decode(&tx_res.as_hex).expect("node returned non-hex transaction").as_slice(),
|
|
||||||
)
|
|
||||||
.expect("couldn't deserialize transaction");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
hex::encode(tx.serialize()),
|
|
||||||
tx_res.as_hex,
|
|
||||||
"Transaction serialization was different"
|
|
||||||
);
|
|
||||||
assert_eq!(tx.hash(), tx_hash, "Transaction hash was different");
|
|
||||||
|
|
||||||
match tx {
|
match tx {
|
||||||
Transaction::V1 { prefix: _, signatures } => {
|
Transaction::V1 { prefix: _, signatures } => {
|
||||||
assert!(!signatures.is_empty());
|
assert!(!signatures.is_empty());
|
||||||
|
|
|
@ -9,7 +9,7 @@ use monero_rpc::{RpcError, Rpc};
|
||||||
use monero_serai::{
|
use monero_serai::{
|
||||||
io::*,
|
io::*,
|
||||||
primitives::Commitment,
|
primitives::Commitment,
|
||||||
transaction::{Timelock, Transaction},
|
transaction::{Timelock, Pruned, Transaction},
|
||||||
block::Block,
|
block::Block,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -108,7 +108,8 @@ impl InternalScanner {
|
||||||
fn scan_transaction(
|
fn scan_transaction(
|
||||||
&self,
|
&self,
|
||||||
tx_start_index_on_blockchain: u64,
|
tx_start_index_on_blockchain: u64,
|
||||||
tx: &Transaction,
|
tx_hash: [u8; 32],
|
||||||
|
tx: &Transaction<Pruned>,
|
||||||
) -> Result<Timelocked, RpcError> {
|
) -> Result<Timelocked, RpcError> {
|
||||||
// Only scan TXs creating RingCT outputs
|
// Only scan TXs creating RingCT outputs
|
||||||
// For the full details on why this check is equivalent, please see the documentation in `scan`
|
// For the full details on why this check is equivalent, please see the documentation in `scan`
|
||||||
|
@ -218,7 +219,7 @@ impl InternalScanner {
|
||||||
|
|
||||||
res.push(WalletOutput {
|
res.push(WalletOutput {
|
||||||
absolute_id: AbsoluteId {
|
absolute_id: AbsoluteId {
|
||||||
transaction: tx.hash(),
|
transaction: tx_hash,
|
||||||
index_in_transaction: o.try_into().unwrap(),
|
index_in_transaction: o.try_into().unwrap(),
|
||||||
},
|
},
|
||||||
relative_id: RelativeId {
|
relative_id: RelativeId {
|
||||||
|
@ -251,8 +252,14 @@ impl InternalScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We obtain all TXs in full
|
// We obtain all TXs in full
|
||||||
let mut txs = vec![block.miner_transaction.clone()];
|
let mut txs_with_hashes = vec![(
|
||||||
txs.extend(rpc.get_transactions(&block.transactions).await?);
|
block.miner_transaction.hash(),
|
||||||
|
Transaction::<Pruned>::from(block.miner_transaction.clone()),
|
||||||
|
)];
|
||||||
|
let txs = rpc.get_pruned_transactions(&block.transactions).await?;
|
||||||
|
for (hash, tx) in block.transactions.iter().zip(txs) {
|
||||||
|
txs_with_hashes.push((*hash, tx));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Requesting the output index for each output we sucessfully scan would cause a loss of privacy
|
Requesting the output index for each output we sucessfully scan would cause a loss of privacy
|
||||||
|
@ -295,13 +302,13 @@ impl InternalScanner {
|
||||||
// Get the starting index
|
// Get the starting index
|
||||||
let mut tx_start_index_on_blockchain = {
|
let mut tx_start_index_on_blockchain = {
|
||||||
let mut tx_start_index_on_blockchain = None;
|
let mut tx_start_index_on_blockchain = None;
|
||||||
for tx in &txs {
|
for (hash, tx) in &txs_with_hashes {
|
||||||
// If this isn't a RingCT output, or there are no outputs, move to the next TX
|
// If this isn't a RingCT output, or there are no outputs, move to the next TX
|
||||||
if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() {
|
if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = *rpc.get_o_indexes(tx.hash()).await?.first().ok_or_else(|| {
|
let index = *rpc.get_o_indexes(*hash).await?.first().ok_or_else(|| {
|
||||||
RpcError::InvalidNode(
|
RpcError::InvalidNode(
|
||||||
"requested output indexes for a TX with outputs and got none".to_string(),
|
"requested output indexes for a TX with outputs and got none".to_string(),
|
||||||
)
|
)
|
||||||
|
@ -317,12 +324,12 @@ impl InternalScanner {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = Timelocked(vec![]);
|
let mut res = Timelocked(vec![]);
|
||||||
for tx in txs {
|
for (hash, tx) in txs_with_hashes {
|
||||||
// Push all outputs into our result
|
// Push all outputs into our result
|
||||||
{
|
{
|
||||||
let mut this_txs_outputs = vec![];
|
let mut this_txs_outputs = vec![];
|
||||||
core::mem::swap(
|
core::mem::swap(
|
||||||
&mut self.scan_transaction(tx_start_index_on_blockchain, &tx)?.0,
|
&mut self.scan_transaction(tx_start_index_on_blockchain, hash, &tx)?.0,
|
||||||
&mut this_txs_outputs,
|
&mut this_txs_outputs,
|
||||||
);
|
);
|
||||||
res.0.extend(this_txs_outputs);
|
res.0.extend(this_txs_outputs);
|
||||||
|
|
|
@ -3,8 +3,8 @@ use std_shims::{vec::Vec, io};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ringct::RctProofs,
|
ringct::PrunedRctProofs,
|
||||||
transaction::{Input, Timelock, Transaction},
|
transaction::{Input, Timelock, Pruned, Transaction},
|
||||||
send::SignableTransaction,
|
send::SignableTransaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ impl Eventuality {
|
||||||
/// intended payments don't match for each other's `Eventuality`s (as they'll have distinct
|
/// intended payments don't match for each other's `Eventuality`s (as they'll have distinct
|
||||||
/// inputs intended).
|
/// inputs intended).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn matches(&self, tx: &Transaction) -> bool {
|
pub fn matches(&self, tx: &Transaction<Pruned>) -> bool {
|
||||||
// Verify extra
|
// Verify extra
|
||||||
if self.0.extra() != tx.prefix().extra {
|
if self.0.extra() != tx.prefix().extra {
|
||||||
return false;
|
return false;
|
||||||
|
@ -91,7 +91,7 @@ impl Eventuality {
|
||||||
|
|
||||||
// Check the encrypted amounts and commitments
|
// Check the encrypted amounts and commitments
|
||||||
let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images);
|
let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images);
|
||||||
let Transaction::V2 { proofs: Some(RctProofs { ref base, .. }), .. } = tx else {
|
let Transaction::V2 { proofs: Some(PrunedRctProofs { ref base, .. }), .. } = tx else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if base.commitments !=
|
if base.commitments !=
|
||||||
|
|
|
@ -66,7 +66,7 @@ test!(
|
||||||
assert_eq!(tx.prefix().extra, eventuality.extra());
|
assert_eq!(tx.prefix().extra, eventuality.extra());
|
||||||
|
|
||||||
// The TX should match
|
// The TX should match
|
||||||
assert!(eventuality.matches(&tx));
|
assert!(eventuality.matches(&tx.clone().into()));
|
||||||
|
|
||||||
// Mutate the TX
|
// Mutate the TX
|
||||||
let Transaction::V2 { proofs: Some(ref mut proofs), .. } = tx else {
|
let Transaction::V2 { proofs: Some(ref mut proofs), .. } = tx else {
|
||||||
|
@ -74,7 +74,7 @@ test!(
|
||||||
};
|
};
|
||||||
proofs.base.commitments[0] += ED25519_BASEPOINT_POINT;
|
proofs.base.commitments[0] += ED25519_BASEPOINT_POINT;
|
||||||
// Verify it no longer matches
|
// Verify it no longer matches
|
||||||
assert!(!eventuality.matches(&tx));
|
assert!(!eventuality.matches(&tx.clone().into()));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -289,7 +289,7 @@ macro_rules! test {
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(&eventuality.extra(), &tx.prefix().extra, "eventuality extra was distinct");
|
assert_eq!(&eventuality.extra(), &tx.prefix().extra, "eventuality extra was distinct");
|
||||||
assert!(eventuality.matches(&tx), "eventuality didn't match");
|
assert!(eventuality.matches(&tx.clone().into()), "eventuality didn't match");
|
||||||
|
|
||||||
tx
|
tx
|
||||||
};
|
};
|
||||||
|
|
|
@ -109,6 +109,7 @@ impl OutputTrait<Monero> for Output {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Consider ([u8; 32], TransactionPruned)
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TransactionTrait<Monero> for Transaction {
|
impl TransactionTrait<Monero> for Transaction {
|
||||||
type Id = [u8; 32];
|
type Id = [u8; 32];
|
||||||
|
@ -575,7 +576,7 @@ impl Network for Monero {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) {
|
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) {
|
||||||
if eventuality.matches(&tx) {
|
if eventuality.matches(&tx.clone().into()) {
|
||||||
res.insert(
|
res.insert(
|
||||||
eventualities.map.remove(&tx.prefix().extra).unwrap().0,
|
eventualities.map.remove(&tx.prefix().extra).unwrap().0,
|
||||||
(block.number().unwrap(), tx.id(), tx),
|
(block.number().unwrap(), tx.id(), tx),
|
||||||
|
@ -681,7 +682,7 @@ impl Network for Monero {
|
||||||
id: &[u8; 32],
|
id: &[u8; 32],
|
||||||
) -> Result<Option<Transaction>, NetworkError> {
|
) -> Result<Option<Transaction>, NetworkError> {
|
||||||
let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?;
|
let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?;
|
||||||
if eventuality.matches(&tx) {
|
if eventuality.matches(&tx.clone().into()) {
|
||||||
Ok(Some(tx))
|
Ok(Some(tx))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -699,7 +700,7 @@ impl Network for Monero {
|
||||||
eventuality: &Self::Eventuality,
|
eventuality: &Self::Eventuality,
|
||||||
claim: &[u8; 32],
|
claim: &[u8; 32],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
return eventuality.matches(&self.rpc.get_transaction(*claim).await.unwrap());
|
return eventuality.matches(&self.rpc.get_pruned_transaction(*claim).await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -711,7 +712,7 @@ impl Network for Monero {
|
||||||
let block = self.rpc.get_block_by_number(block).await.unwrap();
|
let block = self.rpc.get_block_by_number(block).await.unwrap();
|
||||||
for tx in &block.transactions {
|
for tx in &block.transactions {
|
||||||
let tx = self.rpc.get_transaction(*tx).await.unwrap();
|
let tx = self.rpc.get_transaction(*tx).await.unwrap();
|
||||||
if eventuality.matches(&tx) {
|
if eventuality.matches(&tx.clone().into()) {
|
||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue