mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat: generalize Block impls (#14133)
This commit is contained in:
@ -64,7 +64,7 @@ async fn test_get_body() {
|
|||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
// Set a new random block to the mock storage and request it via the network
|
// Set a new random block to the mock storage and request it via the network
|
||||||
let block_hash = rng.gen();
|
let block_hash = rng.gen();
|
||||||
let mut block = Block::default();
|
let mut block: Block = Block::default();
|
||||||
block.body.transactions.push(rng_transaction(&mut rng));
|
block.body.transactions.push(rng_transaction(&mut rng));
|
||||||
|
|
||||||
mock_provider.add_block(block_hash, block.clone());
|
mock_provider.add_block(block_hash, block.clone());
|
||||||
|
|||||||
@ -352,7 +352,7 @@ mod tests {
|
|||||||
const TX: [u8; 251] = hex!("7ef8f8a0a539eb753df3b13b7e386e147d45822b67cb908c9ddc5618e3dbaa22ed00850b94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e2000000558000c5fc50000000000000000000000006605a89f00000000012a10d90000000000000000000000000000000000000000000000000000000af39ac3270000000000000000000000000000000000000000000000000000000d5ea528d24e582fa68786f080069bdbfe06a43f8e67bfd31b8e4d8a8837ba41da9a82a54a0000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985");
|
const TX: [u8; 251] = hex!("7ef8f8a0a539eb753df3b13b7e386e147d45822b67cb908c9ddc5618e3dbaa22ed00850b94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e2000000558000c5fc50000000000000000000000006605a89f00000000012a10d90000000000000000000000000000000000000000000000000000000af39ac3270000000000000000000000000000000000000000000000000000000d5ea528d24e582fa68786f080069bdbfe06a43f8e67bfd31b8e4d8a8837ba41da9a82a54a0000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985");
|
||||||
|
|
||||||
let tx = OpTransactionSigned::decode_2718(&mut TX.as_slice()).unwrap();
|
let tx = OpTransactionSigned::decode_2718(&mut TX.as_slice()).unwrap();
|
||||||
let block = Block {
|
let block: Block<OpTransactionSigned> = Block {
|
||||||
body: BlockBody { transactions: vec![tx], ..Default::default() },
|
body: BlockBody { transactions: vec![tx], ..Default::default() },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@ -293,7 +293,7 @@ mod test {
|
|||||||
OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
|
OpTransactionSigned::decode_2718(&mut TX_1_OP_MAINNET_BLOCK_124665056.as_slice())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let block = Block {
|
let block: Block<OpTransactionSigned> = Block {
|
||||||
body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
|
body: BlockBody { transactions: [tx_0, tx_1.clone()].to_vec(), ..Default::default() },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@ -359,7 +359,7 @@ mod test {
|
|||||||
let system = hex!("7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9");
|
let system = hex!("7ef8f8a0389e292420bcbf9330741f72074e39562a09ff5a00fd22e4e9eee7e34b81bca494deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e20000008dd00101c120000000000000004000000006721035b00000000014189960000000000000000000000000000000000000000000000000000000349b4dcdc000000000000000000000000000000000000000000000000000000004ef9325cc5991ce750960f636ca2ffbb6e209bb3ba91412f21dd78c14ff154d1930f1f9a0000000000000000000000005050f69a9786f081509234f1a7f4684b5e5b76c9");
|
||||||
let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
|
let tx_0 = OpTransactionSigned::decode_2718(&mut &system[..]).unwrap();
|
||||||
|
|
||||||
let block = Block {
|
let block: alloy_consensus::Block<OpTransactionSigned> = Block {
|
||||||
body: BlockBody { transactions: vec![tx_0], ..Default::default() },
|
body: BlockBody { transactions: vec![tx_0], ..Default::default() },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@ -88,7 +88,7 @@ impl PayloadJob for TestPayloadJob {
|
|||||||
fn best_payload(&self) -> Result<EthBuiltPayload, PayloadBuilderError> {
|
fn best_payload(&self) -> Result<EthBuiltPayload, PayloadBuilderError> {
|
||||||
Ok(EthBuiltPayload::new(
|
Ok(EthBuiltPayload::new(
|
||||||
self.attr.payload_id(),
|
self.attr.payload_id(),
|
||||||
Arc::new(Block::default().seal_slow()),
|
Arc::new(Block::<_>::default().seal_slow()),
|
||||||
U256::ZERO,
|
U256::ZERO,
|
||||||
Some(Default::default()),
|
Some(Default::default()),
|
||||||
))
|
))
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use crate::{
|
|||||||
MaybeSerdeBincodeCompat, SignedTransaction,
|
MaybeSerdeBincodeCompat, SignedTransaction,
|
||||||
};
|
};
|
||||||
use alloc::{fmt, vec::Vec};
|
use alloc::{fmt, vec::Vec};
|
||||||
use alloy_consensus::{Header, Transaction, Typed2718};
|
use alloy_consensus::{Transaction, Typed2718};
|
||||||
use alloy_eips::{eip2718::Encodable2718, eip4895::Withdrawals};
|
use alloy_eips::{eip2718::Encodable2718, eip4895::Withdrawals};
|
||||||
use alloy_primitives::{Address, Bytes, B256};
|
use alloy_primitives::{Address, Bytes, B256};
|
||||||
|
|
||||||
@ -177,12 +177,13 @@ pub trait BlockBody:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> BlockBody for alloy_consensus::BlockBody<T>
|
impl<T, H> BlockBody for alloy_consensus::BlockBody<T, H>
|
||||||
where
|
where
|
||||||
T: SignedTransaction,
|
T: SignedTransaction,
|
||||||
|
H: BlockHeader,
|
||||||
{
|
{
|
||||||
type Transaction = T;
|
type Transaction = T;
|
||||||
type OmmerHeader = Header;
|
type OmmerHeader = H;
|
||||||
|
|
||||||
fn transactions(&self) -> &[Self::Transaction] {
|
fn transactions(&self) -> &[Self::Transaction] {
|
||||||
&self.transactions
|
&self.transactions
|
||||||
|
|||||||
@ -11,7 +11,6 @@ pub mod error;
|
|||||||
pub mod header;
|
pub mod header;
|
||||||
|
|
||||||
use alloc::{fmt, vec::Vec};
|
use alloc::{fmt, vec::Vec};
|
||||||
use alloy_consensus::Header;
|
|
||||||
use alloy_primitives::{Address, B256};
|
use alloy_primitives::{Address, B256};
|
||||||
use alloy_rlp::{Decodable, Encodable};
|
use alloy_rlp::{Decodable, Encodable};
|
||||||
|
|
||||||
@ -178,12 +177,13 @@ pub trait Block:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Block for alloy_consensus::Block<T>
|
impl<T, H> Block for alloy_consensus::Block<T, H>
|
||||||
where
|
where
|
||||||
T: SignedTransaction,
|
T: SignedTransaction,
|
||||||
|
H: BlockHeader,
|
||||||
{
|
{
|
||||||
type Header = Header;
|
type Header = H;
|
||||||
type Body = alloy_consensus::BlockBody<T>;
|
type Body = alloy_consensus::BlockBody<T, H>;
|
||||||
|
|
||||||
fn new(header: Self::Header, body: Self::Body) -> Self {
|
fn new(header: Self::Header, body: Self::Body) -> Self {
|
||||||
Self { header, body }
|
Self { header, body }
|
||||||
@ -238,9 +238,10 @@ pub trait TestBlock: Block<Header: crate::test_utils::TestHeader> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-utils"))]
|
#[cfg(any(test, feature = "test-utils"))]
|
||||||
impl<T> TestBlock for alloy_consensus::Block<T>
|
impl<T, H> TestBlock for alloy_consensus::Block<T, H>
|
||||||
where
|
where
|
||||||
T: SignedTransaction,
|
T: SignedTransaction,
|
||||||
|
H: crate::test_utils::TestHeader,
|
||||||
{
|
{
|
||||||
fn body_mut(&mut self) -> &mut Self::Body {
|
fn body_mut(&mut self) -> &mut Self::Body {
|
||||||
&mut self.body
|
&mut self.body
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
//! Test utilities for the block header.
|
//! Test utilities for the block header.
|
||||||
|
|
||||||
|
use crate::BlockHeader;
|
||||||
use alloy_consensus::Header;
|
use alloy_consensus::Header;
|
||||||
use alloy_primitives::{BlockHash, BlockNumber, B256, U256};
|
use alloy_primitives::{BlockHash, BlockNumber, B256, U256};
|
||||||
use proptest::{arbitrary::any, prop_compose};
|
use proptest::{arbitrary::any, prop_compose};
|
||||||
@ -8,7 +9,7 @@ use proptest_arbitrary_interop::arb;
|
|||||||
/// A helper trait for [`Header`]s that allows for mutable access to the headers values.
|
/// A helper trait for [`Header`]s that allows for mutable access to the headers values.
|
||||||
///
|
///
|
||||||
/// This allows for modifying the header for testing purposes.
|
/// This allows for modifying the header for testing purposes.
|
||||||
pub trait TestHeader {
|
pub trait TestHeader: BlockHeader {
|
||||||
/// Updates the parent block hash.
|
/// Updates the parent block hash.
|
||||||
fn set_parent_hash(&mut self, hash: BlockHash);
|
fn set_parent_hash(&mut self, hash: BlockHash);
|
||||||
|
|
||||||
|
|||||||
@ -84,13 +84,13 @@ impl InMemorySize for PooledTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InMemorySize> InMemorySize for alloy_consensus::BlockBody<T> {
|
impl<T: InMemorySize, H: InMemorySize> InMemorySize for alloy_consensus::BlockBody<T, H> {
|
||||||
/// Calculates a heuristic for the in-memory size of the block body
|
/// Calculates a heuristic for the in-memory size of the block body
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
self.transactions.iter().map(T::size).sum::<usize>() +
|
self.transactions.iter().map(T::size).sum::<usize>() +
|
||||||
self.transactions.capacity() * core::mem::size_of::<T>() +
|
self.transactions.capacity() * core::mem::size_of::<T>() +
|
||||||
self.ommers.iter().map(Header::size).sum::<usize>() +
|
self.ommers.iter().map(H::size).sum::<usize>() +
|
||||||
self.ommers.capacity() * core::mem::size_of::<Header>() +
|
self.ommers.capacity() * core::mem::size_of::<Header>() +
|
||||||
self.withdrawals
|
self.withdrawals
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -98,7 +98,7 @@ impl<T: InMemorySize> InMemorySize for alloy_consensus::BlockBody<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InMemorySize> InMemorySize for alloy_consensus::Block<T> {
|
impl<T: InMemorySize, H: InMemorySize> InMemorySize for alloy_consensus::Block<T, H> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
self.header.size() + self.body.size()
|
self.header.size() + self.body.size()
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use alloy_consensus::Header;
|
||||||
use reth_ethereum_primitives::TransactionSigned;
|
use reth_ethereum_primitives::TransactionSigned;
|
||||||
#[cfg(any(test, feature = "arbitrary"))]
|
#[cfg(any(test, feature = "arbitrary"))]
|
||||||
pub use reth_primitives_traits::test_utils::{generate_valid_header, valid_header_strategy};
|
pub use reth_primitives_traits::test_utils::{generate_valid_header, valid_header_strategy};
|
||||||
@ -5,12 +6,12 @@ pub use reth_primitives_traits::test_utils::{generate_valid_header, valid_header
|
|||||||
/// Ethereum full block.
|
/// Ethereum full block.
|
||||||
///
|
///
|
||||||
/// Withdrawals can be optionally included at the end of the RLP encoded message.
|
/// Withdrawals can be optionally included at the end of the RLP encoded message.
|
||||||
pub type Block<T = TransactionSigned> = alloy_consensus::Block<T>;
|
pub type Block<T = TransactionSigned, H = Header> = alloy_consensus::Block<T, H>;
|
||||||
|
|
||||||
/// A response to `GetBlockBodies`, containing bodies if any bodies were found.
|
/// A response to `GetBlockBodies`, containing bodies if any bodies were found.
|
||||||
///
|
///
|
||||||
/// Withdrawals can be optionally included at the end of the RLP encoded message.
|
/// Withdrawals can be optionally included at the end of the RLP encoded message.
|
||||||
pub type BlockBody<T = TransactionSigned> = alloy_consensus::BlockBody<T>;
|
pub type BlockBody<T = TransactionSigned, H = Header> = alloy_consensus::BlockBody<T, H>;
|
||||||
|
|
||||||
/// Ethereum sealed block type
|
/// Ethereum sealed block type
|
||||||
pub type SealedBlock<B = Block> = reth_primitives_traits::block::SealedBlock<B>;
|
pub type SealedBlock<B = Block> = reth_primitives_traits::block::SealedBlock<B>;
|
||||||
|
|||||||
@ -19,7 +19,7 @@ async fn test_basic_engine_calls<C>(client: &C)
|
|||||||
where
|
where
|
||||||
C: ClientT + SubscriptionClientT + Sync + EngineApiClient<EthEngineTypes>,
|
C: ClientT + SubscriptionClientT + Sync + EngineApiClient<EthEngineTypes>,
|
||||||
{
|
{
|
||||||
let block = Block::default().seal_slow();
|
let block = Block::<_>::default().seal_slow();
|
||||||
EngineApiClient::new_payload_v1(client, block_to_payload_v1(block.clone())).await;
|
EngineApiClient::new_payload_v1(client, block_to_payload_v1(block.clone())).await;
|
||||||
EngineApiClient::new_payload_v2(
|
EngineApiClient::new_payload_v2(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::{providers::NodeTypesForProvider, DatabaseProvider};
|
use crate::{providers::NodeTypesForProvider, DatabaseProvider};
|
||||||
use reth_db::transaction::{DbTx, DbTxMut};
|
use reth_db::transaction::{DbTx, DbTxMut};
|
||||||
use reth_node_types::{FullNodePrimitives, FullSignedTx};
|
use reth_node_types::{FullNodePrimitives, FullSignedTx};
|
||||||
|
use reth_primitives_traits::FullBlockHeader;
|
||||||
use reth_storage_api::{ChainStorageReader, ChainStorageWriter, EthStorage};
|
use reth_storage_api::{ChainStorageReader, ChainStorageWriter, EthStorage};
|
||||||
|
|
||||||
/// Trait that provides access to implementations of [`ChainStorage`]
|
/// Trait that provides access to implementations of [`ChainStorage`]
|
||||||
@ -18,13 +19,14 @@ pub trait ChainStorage<Primitives: FullNodePrimitives>: Send + Sync {
|
|||||||
Types: NodeTypesForProvider<Primitives = Primitives>;
|
Types: NodeTypesForProvider<Primitives = Primitives>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N, T> ChainStorage<N> for EthStorage<T>
|
impl<N, T, H> ChainStorage<N> for EthStorage<T, H>
|
||||||
where
|
where
|
||||||
T: FullSignedTx,
|
T: FullSignedTx,
|
||||||
|
H: FullBlockHeader,
|
||||||
N: FullNodePrimitives<
|
N: FullNodePrimitives<
|
||||||
Block = reth_primitives::Block<T>,
|
Block = reth_primitives::Block<T, H>,
|
||||||
BlockHeader = alloy_consensus::Header,
|
BlockHeader = H,
|
||||||
BlockBody = reth_primitives::BlockBody<T>,
|
BlockBody = reth_primitives::BlockBody<T, H>,
|
||||||
SignedTx = T,
|
SignedTx = T,
|
||||||
>,
|
>,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,7 +10,9 @@ use reth_db::{
|
|||||||
DbTxUnwindExt,
|
DbTxUnwindExt,
|
||||||
};
|
};
|
||||||
use reth_primitives::TransactionSigned;
|
use reth_primitives::TransactionSigned;
|
||||||
use reth_primitives_traits::{Block, BlockBody, FullNodePrimitives, SignedTransaction};
|
use reth_primitives_traits::{
|
||||||
|
Block, BlockBody, FullBlockHeader, FullNodePrimitives, SignedTransaction,
|
||||||
|
};
|
||||||
use reth_storage_errors::provider::ProviderResult;
|
use reth_storage_errors::provider::ProviderResult;
|
||||||
|
|
||||||
/// Trait that implements how block bodies are written to the storage.
|
/// Trait that implements how block bodies are written to the storage.
|
||||||
@ -81,26 +83,28 @@ impl<T, Provider, Primitives: FullNodePrimitives> ChainStorageReader<Provider, P
|
|||||||
|
|
||||||
/// Ethereum storage implementation.
|
/// Ethereum storage implementation.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct EthStorage<T = TransactionSigned>(std::marker::PhantomData<T>);
|
pub struct EthStorage<T = TransactionSigned, H = Header>(std::marker::PhantomData<(T, H)>);
|
||||||
|
|
||||||
impl<T> Default for EthStorage<T> {
|
impl<T, H> Default for EthStorage<T, H> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Default::default())
|
Self(Default::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Provider, T> BlockBodyWriter<Provider, reth_primitives::BlockBody<T>> for EthStorage<T>
|
impl<Provider, T, H> BlockBodyWriter<Provider, reth_primitives::BlockBody<T, H>>
|
||||||
|
for EthStorage<T, H>
|
||||||
where
|
where
|
||||||
Provider: DBProvider<Tx: DbTxMut>,
|
Provider: DBProvider<Tx: DbTxMut>,
|
||||||
T: SignedTransaction,
|
T: SignedTransaction,
|
||||||
|
H: FullBlockHeader,
|
||||||
{
|
{
|
||||||
fn write_block_bodies(
|
fn write_block_bodies(
|
||||||
&self,
|
&self,
|
||||||
provider: &Provider,
|
provider: &Provider,
|
||||||
bodies: Vec<(u64, Option<reth_primitives::BlockBody<T>>)>,
|
bodies: Vec<(u64, Option<reth_primitives::BlockBody<T, H>>)>,
|
||||||
_write_to: StorageLocation,
|
_write_to: StorageLocation,
|
||||||
) -> ProviderResult<()> {
|
) -> ProviderResult<()> {
|
||||||
let mut ommers_cursor = provider.tx_ref().cursor_write::<tables::BlockOmmers>()?;
|
let mut ommers_cursor = provider.tx_ref().cursor_write::<tables::BlockOmmers<H>>()?;
|
||||||
let mut withdrawals_cursor =
|
let mut withdrawals_cursor =
|
||||||
provider.tx_ref().cursor_write::<tables::BlockWithdrawals>()?;
|
provider.tx_ref().cursor_write::<tables::BlockWithdrawals>()?;
|
||||||
|
|
||||||
@ -137,14 +141,14 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Provider, T> BlockBodyReader<Provider> for EthStorage<T>
|
impl<Provider, T, H> BlockBodyReader<Provider> for EthStorage<T, H>
|
||||||
where
|
where
|
||||||
Provider: DBProvider
|
Provider:
|
||||||
+ ChainSpecProvider<ChainSpec: EthereumHardforks>
|
DBProvider + ChainSpecProvider<ChainSpec: EthereumHardforks> + OmmersProvider<Header = H>,
|
||||||
+ OmmersProvider<Header = Header>,
|
|
||||||
T: SignedTransaction,
|
T: SignedTransaction,
|
||||||
|
H: FullBlockHeader,
|
||||||
{
|
{
|
||||||
type Block = reth_primitives::Block<T>;
|
type Block = reth_primitives::Block<T, H>;
|
||||||
|
|
||||||
fn read_block_bodies(
|
fn read_block_bodies(
|
||||||
&self,
|
&self,
|
||||||
@ -161,19 +165,19 @@ where
|
|||||||
for (header, transactions) in inputs {
|
for (header, transactions) in inputs {
|
||||||
// If we are past shanghai, then all blocks should have a withdrawal list,
|
// If we are past shanghai, then all blocks should have a withdrawal list,
|
||||||
// even if empty
|
// even if empty
|
||||||
let withdrawals = if chain_spec.is_shanghai_active_at_timestamp(header.timestamp) {
|
let withdrawals = if chain_spec.is_shanghai_active_at_timestamp(header.timestamp()) {
|
||||||
withdrawals_cursor
|
withdrawals_cursor
|
||||||
.seek_exact(header.number)?
|
.seek_exact(header.number())?
|
||||||
.map(|(_, w)| w.withdrawals)
|
.map(|(_, w)| w.withdrawals)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let ommers = if chain_spec.final_paris_total_difficulty(header.number).is_some() {
|
let ommers = if chain_spec.final_paris_total_difficulty(header.number()).is_some() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
provider.ommers(header.number.into())?.unwrap_or_default()
|
provider.ommers(header.number().into())?.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
bodies.push(reth_primitives::BlockBody { transactions, ommers, withdrawals });
|
bodies.push(reth_primitives::BlockBody { transactions, ommers, withdrawals });
|
||||||
|
|||||||
Reference in New Issue
Block a user