mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
Replace "coin" with "network"
The Processor's coins folder referred to the networks it could process, as did its Coin trait. This, and other similar cases throughout the codebase, have now been corrected. Also corrects dated documentation for a key pair is confirmed under the validator-sets pallet.
This commit is contained in:
643
processor/src/networks/monero.rs
Normal file
643
processor/src/networks/monero.rs
Normal file
@@ -0,0 +1,643 @@
|
||||
use std::{time::Duration, collections::HashMap, io};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use ciphersuite::group::{ff::Field, Group};
|
||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||
use frost::{curve::Ed25519, ThresholdKeys};
|
||||
|
||||
use monero_serai::{
|
||||
Protocol,
|
||||
transaction::Transaction,
|
||||
block::Block,
|
||||
rpc::{RpcError, HttpRpc, Rpc},
|
||||
wallet::{
|
||||
ViewPair, Scanner,
|
||||
address::{Network as MoneroNetwork, SubaddressIndex, AddressSpec},
|
||||
Fee, SpendableOutput, Change, Decoys, TransactionError,
|
||||
SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine,
|
||||
},
|
||||
};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub use serai_client::{
|
||||
primitives::{MAX_DATA_LEN, Coin as SeraiCoin, NetworkId, Amount, Balance},
|
||||
networks::monero::Address,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Payment, Plan, additional_key,
|
||||
networks::{
|
||||
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
|
||||
Transaction as TransactionTrait, Eventuality as EventualityTrait, EventualitiesTracker,
|
||||
PostFeeBranch, Network, drop_branches, amortize_fee,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Output(SpendableOutput, Vec<u8>);
|
||||
|
||||
const EXTERNAL_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(0, 0);
|
||||
const BRANCH_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(1, 0);
|
||||
const CHANGE_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(2, 0);
|
||||
|
||||
impl OutputTrait for Output {
|
||||
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug.
|
||||
// While we already are immune, thanks to using featured address, this doesn't hurt and is
|
||||
// technically more efficient.
|
||||
type Id = [u8; 32];
|
||||
|
||||
fn kind(&self) -> OutputType {
|
||||
match self.0.output.metadata.subaddress {
|
||||
EXTERNAL_SUBADDRESS => OutputType::External,
|
||||
BRANCH_SUBADDRESS => OutputType::Branch,
|
||||
CHANGE_SUBADDRESS => OutputType::Change,
|
||||
_ => panic!("unrecognized address was scanned for"),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.0.output.data.key.compress().to_bytes()
|
||||
}
|
||||
|
||||
fn balance(&self) -> Balance {
|
||||
Balance { coin: SeraiCoin::Monero, amount: Amount(self.0.commitment().amount) }
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.1
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.0.write(writer)?;
|
||||
writer.write_all(&u16::try_from(self.1.len()).unwrap().to_le_bytes())?;
|
||||
writer.write_all(&self.1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
let output = SpendableOutput::read(reader)?;
|
||||
|
||||
let mut data_len = [0; 2];
|
||||
reader.read_exact(&mut data_len)?;
|
||||
|
||||
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
|
||||
reader.read_exact(&mut data)?;
|
||||
|
||||
Ok(Output(output, data))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransactionTrait<Monero> for Transaction {
|
||||
type Id = [u8; 32];
|
||||
fn id(&self) -> Self::Id {
|
||||
self.hash()
|
||||
}
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.serialize()
|
||||
}
|
||||
#[cfg(test)]
|
||||
async fn fee(&self, _: &Monero) -> u64 {
|
||||
self.rct_signatures.base.fee
|
||||
}
|
||||
}
|
||||
|
||||
impl EventualityTrait for Eventuality {
|
||||
// Use the TX extra to look up potential matches
|
||||
// While anyone can forge this, a transaction with distinct outputs won't actually match
|
||||
// Extra includess the one time keys which are derived from the plan ID, so a collision here is a
|
||||
// hash collision
|
||||
fn lookup(&self) -> Vec<u8> {
|
||||
self.extra().to_vec()
|
||||
}
|
||||
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
Eventuality::read(reader)
|
||||
}
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignableTransaction {
|
||||
keys: ThresholdKeys<Ed25519>,
|
||||
transcript: RecommendedTranscript,
|
||||
actual: MSignableTransaction,
|
||||
}
|
||||
|
||||
impl BlockTrait<Monero> for Block {
|
||||
type Id = [u8; 32];
|
||||
fn id(&self) -> Self::Id {
|
||||
self.hash()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Self::Id {
|
||||
self.header.previous
|
||||
}
|
||||
|
||||
fn time(&self) -> u64 {
|
||||
self.header.timestamp
|
||||
}
|
||||
|
||||
fn median_fee(&self) -> Fee {
|
||||
// TODO
|
||||
Fee { per_weight: 10000000, mask: 10000 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Monero {
|
||||
rpc: Rpc<HttpRpc>,
|
||||
}
|
||||
// Shim required for testing/debugging purposes due to generic arguments also necessitating trait
|
||||
// bounds
|
||||
impl PartialEq for Monero {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl Eq for Monero {}
|
||||
|
||||
impl Monero {
|
||||
pub fn new(url: String) -> Monero {
|
||||
Monero { rpc: HttpRpc::new(url).unwrap() }
|
||||
}
|
||||
|
||||
fn view_pair(spend: EdwardsPoint) -> ViewPair {
|
||||
ViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0))
|
||||
}
|
||||
|
||||
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
|
||||
Address::new(Self::view_pair(spend).address(
|
||||
MoneroNetwork::Mainnet,
|
||||
AddressSpec::Featured { subaddress, payment_id: None, guaranteed: true },
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn scanner(spend: EdwardsPoint) -> Scanner {
|
||||
let mut scanner = Scanner::from_view(Self::view_pair(spend), None);
|
||||
debug_assert!(EXTERNAL_SUBADDRESS.is_none());
|
||||
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
|
||||
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
|
||||
scanner
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_view_pair() -> ViewPair {
|
||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_scanner() -> Scanner {
|
||||
Scanner::from_view(Self::test_view_pair(), Some(std::collections::HashSet::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_address() -> Address {
|
||||
Address::new(Self::test_view_pair().address(MoneroNetwork::Mainnet, AddressSpec::Standard))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Network for Monero {
|
||||
type Curve = Ed25519;
|
||||
|
||||
type Fee = Fee;
|
||||
type Transaction = Transaction;
|
||||
type Block = Block;
|
||||
|
||||
type Output = Output;
|
||||
type SignableTransaction = SignableTransaction;
|
||||
type Eventuality = Eventuality;
|
||||
type TransactionMachine = TransactionMachine;
|
||||
|
||||
type Address = Address;
|
||||
|
||||
const NETWORK: NetworkId = NetworkId::Monero;
|
||||
const ID: &'static str = "Monero";
|
||||
const CONFIRMATIONS: usize = 10;
|
||||
|
||||
// wallet2 will not create a transaction larger than 100kb, and Monero won't relay a transaction
|
||||
// larger than 150kb. This fits within the 100kb mark
|
||||
// Technically, it can be ~124, yet a small bit of buffer is appreciated
|
||||
// TODO: Test creating a TX this big
|
||||
const MAX_INPUTS: usize = 120;
|
||||
const MAX_OUTPUTS: usize = 16;
|
||||
|
||||
// 0.01 XMR
|
||||
const DUST: u64 = 10000000000;
|
||||
|
||||
// Monero doesn't require/benefit from tweaking
|
||||
fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {}
|
||||
|
||||
fn address(key: EdwardsPoint) -> Self::Address {
|
||||
Self::address_internal(key, EXTERNAL_SUBADDRESS)
|
||||
}
|
||||
|
||||
fn branch_address(key: EdwardsPoint) -> Self::Address {
|
||||
Self::address_internal(key, BRANCH_SUBADDRESS)
|
||||
}
|
||||
|
||||
async fn get_latest_block_number(&self) -> Result<usize, NetworkError> {
|
||||
// Monero defines height as chain length, so subtract 1 for block number
|
||||
Ok(self.rpc.get_height().await.map_err(|_| NetworkError::ConnectionError)? - 1)
|
||||
}
|
||||
|
||||
async fn get_block(&self, number: usize) -> Result<Self::Block, NetworkError> {
|
||||
Ok(
|
||||
self
|
||||
.rpc
|
||||
.get_block(
|
||||
self.rpc.get_block_hash(number).await.map_err(|_| NetworkError::ConnectionError)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| NetworkError::ConnectionError)?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_outputs(
|
||||
&self,
|
||||
block: &Block,
|
||||
key: EdwardsPoint,
|
||||
) -> Result<Vec<Self::Output>, NetworkError> {
|
||||
let mut txs = Self::scanner(key)
|
||||
.scan(&self.rpc, block)
|
||||
.await
|
||||
.map_err(|_| NetworkError::ConnectionError)?
|
||||
.iter()
|
||||
.filter_map(|outputs| Some(outputs.not_locked()).filter(|outputs| !outputs.is_empty()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// This should be pointless as we shouldn't be able to scan for any other subaddress
|
||||
// This just ensures nothing invalid makes it through
|
||||
for tx_outputs in &txs {
|
||||
for output in tx_outputs {
|
||||
assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS]
|
||||
.contains(&output.output.metadata.subaddress));
|
||||
}
|
||||
}
|
||||
|
||||
let mut outputs = Vec::with_capacity(txs.len());
|
||||
for mut tx_outputs in txs.drain(..) {
|
||||
for output in tx_outputs.drain(..) {
|
||||
let mut data = output.arbitrary_data().get(0).cloned().unwrap_or(vec![]);
|
||||
|
||||
// The Output serialization code above uses u16 to represent length
|
||||
data.truncate(u16::MAX.into());
|
||||
// Monero data segments should be <= 255 already, and MAX_DATA_LEN is currently 512
|
||||
// This just allows either Monero to change, or MAX_DATA_LEN to change, without introducing
|
||||
// complicationso
|
||||
data.truncate(MAX_DATA_LEN.try_into().unwrap());
|
||||
|
||||
outputs.push(Output(output, data));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
async fn get_eventuality_completions(
|
||||
&self,
|
||||
eventualities: &mut EventualitiesTracker<Eventuality>,
|
||||
block: &Block,
|
||||
) -> HashMap<[u8; 32], [u8; 32]> {
|
||||
let mut res = HashMap::new();
|
||||
if eventualities.map.is_empty() {
|
||||
return res;
|
||||
}
|
||||
|
||||
async fn check_block(
|
||||
network: &Monero,
|
||||
eventualities: &mut EventualitiesTracker<Eventuality>,
|
||||
block: &Block,
|
||||
res: &mut HashMap<[u8; 32], [u8; 32]>,
|
||||
) {
|
||||
for hash in &block.txs {
|
||||
let tx = {
|
||||
let mut tx;
|
||||
while {
|
||||
tx = network.get_transaction(hash).await;
|
||||
tx.is_err()
|
||||
} {
|
||||
log::error!("couldn't get transaction {}: {}", hex::encode(hash), tx.err().unwrap());
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
tx.unwrap()
|
||||
};
|
||||
|
||||
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix.extra) {
|
||||
if eventuality.matches(&tx) {
|
||||
res.insert(eventualities.map.remove(&tx.prefix.extra).unwrap().0, tx.hash());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventualities.block_number += 1;
|
||||
assert_eq!(eventualities.block_number, block.number());
|
||||
}
|
||||
|
||||
for block_num in (eventualities.block_number + 1) .. block.number() {
|
||||
let block = {
|
||||
let mut block;
|
||||
while {
|
||||
block = self.get_block(block_num).await;
|
||||
block.is_err()
|
||||
} {
|
||||
log::error!("couldn't get block {}: {}", block_num, block.err().unwrap());
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
block.unwrap()
|
||||
};
|
||||
|
||||
check_block(self, eventualities, &block, &mut res).await;
|
||||
}
|
||||
|
||||
// Also check the current block
|
||||
check_block(self, eventualities, block, &mut res).await;
|
||||
assert_eq!(eventualities.block_number, block.number());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn prepare_send(
|
||||
&self,
|
||||
keys: ThresholdKeys<Ed25519>,
|
||||
block_number: usize,
|
||||
mut plan: Plan<Self>,
|
||||
fee: Fee,
|
||||
) -> Result<(Option<(SignableTransaction, Eventuality)>, Vec<PostFeeBranch>), NetworkError> {
|
||||
// Sanity check this has at least one output planned
|
||||
assert!((!plan.payments.is_empty()) || plan.change.is_some());
|
||||
|
||||
// Get the protocol for the specified block number
|
||||
// For now, this should just be v16, the latest deployed protocol, since there's no upcoming
|
||||
// hard fork to be mindful of
|
||||
let get_protocol = || Protocol::v16;
|
||||
|
||||
#[cfg(not(test))]
|
||||
let protocol = get_protocol();
|
||||
// If this is a test, we won't be using a mainnet node and need a distinct protocol
|
||||
// determination
|
||||
// Just use whatever the node expects
|
||||
#[cfg(test)]
|
||||
let protocol = self.rpc.get_protocol().await.unwrap();
|
||||
|
||||
// Hedge against the above codegen failing by having an always included runtime check
|
||||
if !cfg!(test) {
|
||||
assert_eq!(protocol, get_protocol());
|
||||
}
|
||||
|
||||
// Check a fork hasn't occurred which this processor hasn't been updated for
|
||||
assert_eq!(protocol, self.rpc.get_protocol().await.map_err(|_| NetworkError::ConnectionError)?);
|
||||
|
||||
let spendable_outputs = plan.inputs.iter().cloned().map(|input| input.0).collect::<Vec<_>>();
|
||||
|
||||
let mut transcript = plan.transcript();
|
||||
|
||||
// All signers need to select the same decoys
|
||||
// All signers use the same height and a seeded RNG to make sure they do so.
|
||||
let decoys = Decoys::select(
|
||||
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
|
||||
&self.rpc,
|
||||
protocol.ring_len(),
|
||||
block_number + 1,
|
||||
&spendable_outputs,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| NetworkError::ConnectionError)
|
||||
.unwrap();
|
||||
|
||||
let inputs = spendable_outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
|
||||
|
||||
let signable = |mut plan: Plan<Self>, tx_fee: Option<_>| {
|
||||
// Monero requires at least two outputs
|
||||
// If we only have one output planned, add a dummy payment
|
||||
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
|
||||
if outputs == 0 {
|
||||
return Ok(None);
|
||||
} else if outputs == 1 {
|
||||
plan.payments.push(Payment {
|
||||
address: Address::new(
|
||||
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
||||
.address(MoneroNetwork::Mainnet, AddressSpec::Standard),
|
||||
)
|
||||
.unwrap(),
|
||||
amount: 0,
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut payments = vec![];
|
||||
for payment in &plan.payments {
|
||||
// If we're solely estimating the fee, don't actually specify an amount
|
||||
// This won't affect the fee calculation yet will ensure we don't hit an out of funds error
|
||||
payments.push((
|
||||
payment.address.clone().into(),
|
||||
if tx_fee.is_none() { 0 } else { payment.amount },
|
||||
));
|
||||
}
|
||||
|
||||
match MSignableTransaction::new(
|
||||
protocol,
|
||||
// Use the plan ID as the r_seed
|
||||
// This perfectly binds the plan while simultaneously allowing verifying the plan was
|
||||
// executed with no additional communication
|
||||
Some(Zeroizing::new(plan.id())),
|
||||
inputs.clone(),
|
||||
payments,
|
||||
plan.change.map(|key| {
|
||||
Change::fingerprintable(Self::address_internal(key, CHANGE_SUBADDRESS).into())
|
||||
}),
|
||||
vec![],
|
||||
fee,
|
||||
) {
|
||||
Ok(signable) => Ok(Some(signable)),
|
||||
Err(e) => match e {
|
||||
TransactionError::MultiplePaymentIds => {
|
||||
panic!("multiple payment IDs despite not supporting integrated addresses");
|
||||
}
|
||||
TransactionError::NoInputs |
|
||||
TransactionError::NoOutputs |
|
||||
TransactionError::InvalidDecoyQuantity |
|
||||
TransactionError::NoChange |
|
||||
TransactionError::TooManyOutputs |
|
||||
TransactionError::TooMuchData |
|
||||
TransactionError::TooLargeTransaction |
|
||||
TransactionError::WrongPrivateKey => {
|
||||
panic!("created an Monero invalid transaction: {e}");
|
||||
}
|
||||
TransactionError::ClsagError(_) |
|
||||
TransactionError::InvalidTransaction(_) |
|
||||
TransactionError::FrostError(_) => {
|
||||
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
||||
}
|
||||
TransactionError::NotEnoughFunds { inputs, outputs, fee } => {
|
||||
if let Some(tx_fee) = tx_fee {
|
||||
panic!(
|
||||
"{}. in: {inputs}, out: {outputs}, fee: {fee}, prior estimated fee: {tx_fee}",
|
||||
"didn't have enough funds for a Monero TX",
|
||||
);
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
TransactionError::RpcError(e) => {
|
||||
log::error!("RpcError when preparing transaction: {e:?}");
|
||||
Err(NetworkError::ConnectionError)
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let tx_fee = match signable(plan.clone(), None)? {
|
||||
Some(tx) => tx.fee(),
|
||||
None => return Ok((None, drop_branches(&plan))),
|
||||
};
|
||||
|
||||
let branch_outputs = amortize_fee(&mut plan, tx_fee);
|
||||
|
||||
let signable = SignableTransaction {
|
||||
keys,
|
||||
transcript,
|
||||
actual: match signable(plan, Some(tx_fee))? {
|
||||
Some(signable) => signable,
|
||||
None => return Ok((None, branch_outputs)),
|
||||
},
|
||||
};
|
||||
let eventuality = signable.actual.eventuality().unwrap();
|
||||
Ok((Some((signable, eventuality)), branch_outputs))
|
||||
}
|
||||
|
||||
async fn attempt_send(
|
||||
&self,
|
||||
transaction: SignableTransaction,
|
||||
) -> Result<Self::TransactionMachine, NetworkError> {
|
||||
match transaction.actual.clone().multisig(transaction.keys.clone(), transaction.transcript) {
|
||||
Ok(machine) => Ok(machine),
|
||||
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_transaction(&self, tx: &Self::Transaction) -> Result<(), NetworkError> {
|
||||
match self.rpc.publish_transaction(tx).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(RpcError::ConnectionError) => Err(NetworkError::ConnectionError)?,
|
||||
// TODO: Distinguish already in pool vs double spend (other signing attempt succeeded) vs
|
||||
// invalid transaction
|
||||
Err(e) => panic!("failed to publish TX {}: {e}", hex::encode(tx.hash())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_transaction(&self, id: &[u8; 32]) -> Result<Transaction, NetworkError> {
|
||||
self.rpc.get_transaction(*id).await.map_err(|_| NetworkError::ConnectionError)
|
||||
}
|
||||
|
||||
fn confirm_completion(&self, eventuality: &Eventuality, tx: &Transaction) -> bool {
|
||||
eventuality.matches(tx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
|
||||
self.rpc.get_block(*id).await.unwrap().number()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn get_fee(&self) -> Self::Fee {
|
||||
use monero_serai::wallet::FeePriority;
|
||||
|
||||
self.rpc.get_fee(self.rpc.get_protocol().await.unwrap(), FeePriority::Low).await.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn mine_block(&self) {
|
||||
// https://github.com/serai-dex/serai/issues/198
|
||||
sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct EmptyResponse {}
|
||||
let _: EmptyResponse = self
|
||||
.rpc
|
||||
.rpc_call(
|
||||
"json_rpc",
|
||||
Some(serde_json::json!({
|
||||
"method": "generateblocks",
|
||||
"params": {
|
||||
"wallet_address": Self::test_address().to_string(),
|
||||
"amount_of_blocks": 1
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn test_send(&self, address: Self::Address) -> Block {
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::OsRng;
|
||||
use monero_serai::wallet::FeePriority;
|
||||
|
||||
let new_block = self.get_latest_block_number().await.unwrap() + 1;
|
||||
for _ in 0 .. 80 {
|
||||
self.mine_block().await;
|
||||
}
|
||||
|
||||
let outputs = Self::test_scanner()
|
||||
.scan(&self.rpc, &self.rpc.get_block_by_number(new_block).await.unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.swap_remove(0)
|
||||
.ignore_timelock();
|
||||
|
||||
let amount = outputs[0].commitment().amount;
|
||||
// The dust should always be sufficient for the fee
|
||||
let fee = Monero::DUST;
|
||||
|
||||
let protocol = self.rpc.get_protocol().await.unwrap();
|
||||
|
||||
let decoys = Decoys::select(
|
||||
&mut OsRng,
|
||||
&self.rpc,
|
||||
protocol.ring_len(),
|
||||
self.rpc.get_height().await.unwrap() - 1,
|
||||
&outputs,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let inputs = outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
|
||||
|
||||
let tx = MSignableTransaction::new(
|
||||
protocol,
|
||||
None,
|
||||
inputs,
|
||||
vec![(address.into(), amount - fee)],
|
||||
Some(Change::fingerprintable(Self::test_address().into())),
|
||||
vec![],
|
||||
self.rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let block = self.get_latest_block_number().await.unwrap() + 1;
|
||||
self.rpc.publish_transaction(&tx).await.unwrap();
|
||||
for _ in 0 .. 10 {
|
||||
self.mine_block().await;
|
||||
}
|
||||
self.get_block(block).await.unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user