mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-21 22:58:46 +00:00
Remove Output::amount and move Payment from Amount to Balance
This code is still largely designed around the idea a payment for a network is fungible with any other, which isn't true. This starts moving past that. Asserts are added to ensure the integrity of coin to the scheduler (which is now per key per coin, not per key alone) and in Bitcoin/Monero prepare_send.
This commit is contained in:
parent
ffedba7a05
commit
7d72e224f0
10 changed files with 201 additions and 87 deletions
|
@ -5,10 +5,9 @@ use ciphersuite::Ciphersuite;
|
||||||
pub use serai_db::*;
|
pub use serai_db::*;
|
||||||
|
|
||||||
use scale::{Encode, Decode};
|
use scale::{Encode, Decode};
|
||||||
#[rustfmt::skip]
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::ExternalAddress,
|
primitives::{Balance, ExternalAddress},
|
||||||
in_instructions::primitives::InInstructionWithBalance
|
in_instructions::primitives::InInstructionWithBalance,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -172,23 +171,23 @@ impl<N: Network, D: Db> MultisigsDb<N, D> {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forwarded_output_key(amount: u64) -> Vec<u8> {
|
fn forwarded_output_key(balance: Balance) -> Vec<u8> {
|
||||||
Self::multisigs_key(b"forwarded_output", amount.to_le_bytes())
|
Self::multisigs_key(b"forwarded_output", balance.encode())
|
||||||
}
|
}
|
||||||
pub fn save_forwarded_output(
|
pub fn save_forwarded_output(
|
||||||
txn: &mut D::Transaction<'_>,
|
txn: &mut D::Transaction<'_>,
|
||||||
instruction: InInstructionWithBalance,
|
instruction: InInstructionWithBalance,
|
||||||
) {
|
) {
|
||||||
let key = Self::forwarded_output_key(instruction.balance.amount.0);
|
let key = Self::forwarded_output_key(instruction.balance);
|
||||||
let mut existing = txn.get(&key).unwrap_or(vec![]);
|
let mut existing = txn.get(&key).unwrap_or(vec![]);
|
||||||
existing.extend(instruction.encode());
|
existing.extend(instruction.encode());
|
||||||
txn.put(key, existing);
|
txn.put(key, existing);
|
||||||
}
|
}
|
||||||
pub fn take_forwarded_output(
|
pub fn take_forwarded_output(
|
||||||
txn: &mut D::Transaction<'_>,
|
txn: &mut D::Transaction<'_>,
|
||||||
amount: u64,
|
balance: Balance,
|
||||||
) -> Option<InInstructionWithBalance> {
|
) -> Option<InInstructionWithBalance> {
|
||||||
let key = Self::forwarded_output_key(amount);
|
let key = Self::forwarded_output_key(balance);
|
||||||
|
|
||||||
let outputs = txn.get(&key)?;
|
let outputs = txn.get(&key)?;
|
||||||
let mut outputs_ref = outputs.as_slice();
|
let mut outputs_ref = outputs.as_slice();
|
||||||
|
|
|
@ -7,7 +7,7 @@ use scale::{Encode, Decode};
|
||||||
use messages::SubstrateContext;
|
use messages::SubstrateContext;
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{MAX_DATA_LEN, ExternalAddress, BlockHash},
|
primitives::{MAX_DATA_LEN, NetworkId, Coin, ExternalAddress, BlockHash},
|
||||||
in_instructions::primitives::{
|
in_instructions::primitives::{
|
||||||
InInstructionWithBalance, Batch, RefundableInInstruction, Shorthand, MAX_BATCH_SIZE,
|
InInstructionWithBalance, Batch, RefundableInInstruction, Shorthand, MAX_BATCH_SIZE,
|
||||||
},
|
},
|
||||||
|
@ -157,7 +157,20 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
assert!(current_keys.len() <= 2);
|
assert!(current_keys.len() <= 2);
|
||||||
let mut actively_signing = vec![];
|
let mut actively_signing = vec![];
|
||||||
for (_, key) in ¤t_keys {
|
for (_, key) in ¤t_keys {
|
||||||
schedulers.push(Scheduler::from_db(raw_db, *key).unwrap());
|
schedulers.push(
|
||||||
|
Scheduler::from_db(
|
||||||
|
raw_db,
|
||||||
|
*key,
|
||||||
|
match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("adding a key for Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
// TODO: This is incomplete to DAI
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
// Load any TXs being actively signed
|
// Load any TXs being actively signed
|
||||||
let key = key.to_bytes();
|
let key = key.to_bytes();
|
||||||
|
@ -234,7 +247,17 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
let viewer = Some(MultisigViewer {
|
let viewer = Some(MultisigViewer {
|
||||||
activation_block,
|
activation_block,
|
||||||
key: external_key,
|
key: external_key,
|
||||||
scheduler: Scheduler::<N>::new::<D>(txn, external_key),
|
scheduler: Scheduler::<N>::new::<D>(
|
||||||
|
txn,
|
||||||
|
external_key,
|
||||||
|
match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("adding a key for Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
// TODO: This is incomplete to DAI
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.existing.is_none() {
|
if self.existing.is_none() {
|
||||||
|
@ -295,12 +318,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
assert_eq!(balance.coin.network(), N::NETWORK);
|
assert_eq!(balance.coin.network(), N::NETWORK);
|
||||||
|
|
||||||
if let Ok(address) = N::Address::try_from(address.consume()) {
|
if let Ok(address) = N::Address::try_from(address.consume()) {
|
||||||
// TODO: Add coin to payment
|
payments.push(Payment { address, data: data.map(|data| data.consume()), balance });
|
||||||
payments.push(Payment {
|
|
||||||
address,
|
|
||||||
data: data.map(|data| data.consume()),
|
|
||||||
amount: balance.amount.0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +362,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
inputs: vec![output.clone()],
|
inputs: vec![output.clone()],
|
||||||
// Uses a payment as this will still be successfully sent due to fee amortization,
|
// Uses a payment as this will still be successfully sent due to fee amortization,
|
||||||
// and because change is currently always a Serai key
|
// and because change is currently always a Serai key
|
||||||
payments: vec![Payment { address: refund_to, data: None, amount: output.balance().amount.0 }],
|
payments: vec![Payment { address: refund_to, data: None, balance: output.balance() }],
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -542,7 +560,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
//
|
//
|
||||||
// This is unnecessary, due to the current flow around Eventuality resolutions and the
|
// This is unnecessary, due to the current flow around Eventuality resolutions and the
|
||||||
// current bounds naturally found being sufficiently amenable, yet notable for the future
|
// current bounds naturally found being sufficiently amenable, yet notable for the future
|
||||||
if scheduler.can_use_branch(output.amount()) {
|
if scheduler.can_use_branch(output.balance()) {
|
||||||
// We could simply call can_use_branch, yet it'd have an edge case where if we receive
|
// We could simply call can_use_branch, yet it'd have an edge case where if we receive
|
||||||
// two outputs for 100, and we could use one such output, we'd handle both.
|
// two outputs for 100, and we could use one such output, we'd handle both.
|
||||||
//
|
//
|
||||||
|
@ -852,7 +870,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(instruction) =
|
if let Some(instruction) =
|
||||||
MultisigsDb::<N, D>::take_forwarded_output(txn, output.amount())
|
MultisigsDb::<N, D>::take_forwarded_output(txn, output.balance())
|
||||||
{
|
{
|
||||||
instruction
|
instruction
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -553,7 +553,7 @@ impl<N: Network, D: Db> Scanner<N, D> {
|
||||||
// TODO: These lines are the ones which will cause a really long-lived lock acquisiton
|
// TODO: These lines are the ones which will cause a really long-lived lock acquisiton
|
||||||
for output in network.get_outputs(&block, key).await {
|
for output in network.get_outputs(&block, key).await {
|
||||||
assert_eq!(output.key(), key);
|
assert_eq!(output.key(), key);
|
||||||
if output.amount() >= N::DUST {
|
if output.balance().amount.0 >= N::DUST {
|
||||||
outputs.push(output);
|
outputs.push(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -580,10 +580,10 @@ impl<N: Network, D: Db> Scanner<N, D> {
|
||||||
for output in &outputs {
|
for output in &outputs {
|
||||||
let id = output.id();
|
let id = output.id();
|
||||||
info!(
|
info!(
|
||||||
"block {} had output {} worth {}",
|
"block {} had output {} worth {:?}",
|
||||||
hex::encode(&block_id),
|
hex::encode(&block_id),
|
||||||
hex::encode(&id),
|
hex::encode(&id),
|
||||||
output.amount(),
|
output.balance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// On Bitcoin, the output ID should be unique for a given chain
|
// On Bitcoin, the output ID should be unique for a given chain
|
||||||
|
|
|
@ -5,6 +5,8 @@ use std::{
|
||||||
|
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||||
|
|
||||||
|
use serai_client::primitives::{Coin, Amount, Balance};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
networks::{OutputType, Output, Network},
|
networks::{OutputType, Output, Network},
|
||||||
DbTxn, Db, Payment, Plan,
|
DbTxn, Db, Payment, Plan,
|
||||||
|
@ -14,6 +16,7 @@ use crate::{
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub struct Scheduler<N: Network> {
|
pub struct Scheduler<N: Network> {
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
key: <N::Curve as Ciphersuite>::G,
|
||||||
|
coin: Coin,
|
||||||
|
|
||||||
// Serai, when it has more outputs expected than it can handle in a single tranaction, will
|
// Serai, when it has more outputs expected than it can handle in a single tranaction, will
|
||||||
// schedule the outputs to be handled later. Immediately, it just creates additional outputs
|
// schedule the outputs to be handled later. Immediately, it just creates additional outputs
|
||||||
|
@ -51,7 +54,11 @@ impl<N: Network> Scheduler<N> {
|
||||||
self.payments.is_empty()
|
self.payments.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read<R: Read>(key: <N::Curve as Ciphersuite>::G, reader: &mut R) -> io::Result<Self> {
|
fn read<R: Read>(
|
||||||
|
key: <N::Curve as Ciphersuite>::G,
|
||||||
|
coin: Coin,
|
||||||
|
reader: &mut R,
|
||||||
|
) -> io::Result<Self> {
|
||||||
let mut read_plans = || -> io::Result<_> {
|
let mut read_plans = || -> io::Result<_> {
|
||||||
let mut all_plans = HashMap::new();
|
let mut all_plans = HashMap::new();
|
||||||
let mut all_plans_len = [0; 4];
|
let mut all_plans_len = [0; 4];
|
||||||
|
@ -95,7 +102,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
payments.push_back(Payment::read(reader)?);
|
payments.push_back(Payment::read(reader)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Scheduler { key, queued_plans, plans, utxos, payments })
|
Ok(Scheduler { key, coin, queued_plans, plans, utxos, payments })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO2: Get rid of this
|
// TODO2: Get rid of this
|
||||||
|
@ -130,13 +137,18 @@ impl<N: Network> Scheduler<N> {
|
||||||
payment.write(&mut res).unwrap();
|
payment.write(&mut res).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_assert_eq!(&Self::read(self.key, &mut res.as_slice()).unwrap(), self);
|
debug_assert_eq!(&Self::read(self.key, self.coin, &mut res.as_slice()).unwrap(), self);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new<D: Db>(txn: &mut D::Transaction<'_>, key: <N::Curve as Ciphersuite>::G) -> Self {
|
pub fn new<D: Db>(
|
||||||
|
txn: &mut D::Transaction<'_>,
|
||||||
|
key: <N::Curve as Ciphersuite>::G,
|
||||||
|
coin: Coin,
|
||||||
|
) -> Self {
|
||||||
let res = Scheduler {
|
let res = Scheduler {
|
||||||
key,
|
key,
|
||||||
|
coin,
|
||||||
queued_plans: HashMap::new(),
|
queued_plans: HashMap::new(),
|
||||||
plans: HashMap::new(),
|
plans: HashMap::new(),
|
||||||
utxos: vec![],
|
utxos: vec![],
|
||||||
|
@ -147,18 +159,19 @@ impl<N: Network> Scheduler<N> {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_db<D: Db>(db: &D, key: <N::Curve as Ciphersuite>::G) -> io::Result<Self> {
|
pub fn from_db<D: Db>(db: &D, key: <N::Curve as Ciphersuite>::G, coin: Coin) -> io::Result<Self> {
|
||||||
let scheduler = db.get(scheduler_key::<D, _>(&key)).unwrap_or_else(|| {
|
let scheduler = db.get(scheduler_key::<D, _>(&key)).unwrap_or_else(|| {
|
||||||
panic!("loading scheduler from DB without scheduler for {}", hex::encode(key.to_bytes()))
|
panic!("loading scheduler from DB without scheduler for {}", hex::encode(key.to_bytes()))
|
||||||
});
|
});
|
||||||
let mut reader_slice = scheduler.as_slice();
|
let mut reader_slice = scheduler.as_slice();
|
||||||
let reader = &mut reader_slice;
|
let reader = &mut reader_slice;
|
||||||
|
|
||||||
Self::read(key, reader)
|
Self::read(key, coin, reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_use_branch(&self, amount: u64) -> bool {
|
pub fn can_use_branch(&self, balance: Balance) -> bool {
|
||||||
self.plans.contains_key(&amount)
|
assert_eq!(balance.coin, self.coin);
|
||||||
|
self.plans.contains_key(&balance.amount.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(
|
fn execute(
|
||||||
|
@ -170,11 +183,14 @@ impl<N: Network> Scheduler<N> {
|
||||||
let mut change = false;
|
let mut change = false;
|
||||||
let mut max = N::MAX_OUTPUTS;
|
let mut max = N::MAX_OUTPUTS;
|
||||||
|
|
||||||
let payment_amounts =
|
let payment_amounts = |payments: &Vec<Payment<N>>| {
|
||||||
|payments: &Vec<Payment<N>>| payments.iter().map(|payment| payment.amount).sum::<u64>();
|
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>()
|
||||||
|
};
|
||||||
|
|
||||||
// Requires a change output
|
// Requires a change output
|
||||||
if inputs.iter().map(Output::amount).sum::<u64>() != payment_amounts(&payments) {
|
if inputs.iter().map(|output| output.balance().amount.0).sum::<u64>() !=
|
||||||
|
payment_amounts(&payments)
|
||||||
|
{
|
||||||
change = true;
|
change = true;
|
||||||
max -= 1;
|
max -= 1;
|
||||||
}
|
}
|
||||||
|
@ -208,7 +224,14 @@ impl<N: Network> Scheduler<N> {
|
||||||
|
|
||||||
// Create the payment for the plan
|
// Create the payment for the plan
|
||||||
// Push it to the front so it's not moved into a branch until all lower-depth items are
|
// Push it to the front so it's not moved into a branch until all lower-depth items are
|
||||||
payments.insert(0, Payment { address: branch_address.clone(), data: None, amount });
|
payments.insert(
|
||||||
|
0,
|
||||||
|
Payment {
|
||||||
|
address: branch_address.clone(),
|
||||||
|
data: None,
|
||||||
|
balance: Balance { coin: self.coin, amount: Amount(amount) },
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plan {
|
Plan {
|
||||||
|
@ -230,12 +253,12 @@ impl<N: Network> Scheduler<N> {
|
||||||
|
|
||||||
for utxo in utxos.drain(..) {
|
for utxo in utxos.drain(..) {
|
||||||
if utxo.kind() == OutputType::Branch {
|
if utxo.kind() == OutputType::Branch {
|
||||||
let amount = utxo.amount();
|
let amount = utxo.balance().amount.0;
|
||||||
if let Some(plans) = self.plans.get_mut(&amount) {
|
if let Some(plans) = self.plans.get_mut(&amount) {
|
||||||
// Execute the first set of payments possible with an output of this amount
|
// Execute the first set of payments possible with an output of this amount
|
||||||
let payments = plans.pop_front().unwrap();
|
let payments = plans.pop_front().unwrap();
|
||||||
// They won't be equal if we dropped payments due to being dust
|
// They won't be equal if we dropped payments due to being dust
|
||||||
assert!(amount >= payments.iter().map(|payment| payment.amount).sum::<u64>());
|
assert!(amount >= payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
||||||
|
|
||||||
// If we've grabbed the last plan for this output amount, remove it from the map
|
// If we've grabbed the last plan for this output amount, remove it from the map
|
||||||
if plans.is_empty() {
|
if plans.is_empty() {
|
||||||
|
@ -264,6 +287,13 @@ impl<N: Network> Scheduler<N> {
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
||||||
force_spend: bool,
|
force_spend: bool,
|
||||||
) -> Vec<Plan<N>> {
|
) -> Vec<Plan<N>> {
|
||||||
|
for utxo in &utxos {
|
||||||
|
assert_eq!(utxo.balance().coin, self.coin);
|
||||||
|
}
|
||||||
|
for payment in &payments {
|
||||||
|
assert_eq!(payment.balance.coin, self.coin);
|
||||||
|
}
|
||||||
|
|
||||||
// Drop payments to our own branch address
|
// Drop payments to our own branch address
|
||||||
/*
|
/*
|
||||||
created_output will be called any time we send to a branch address. If it's called, and it
|
created_output will be called any time we send to a branch address. If it's called, and it
|
||||||
|
@ -297,7 +327,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort UTXOs so the highest valued ones are first
|
// Sort UTXOs so the highest valued ones are first
|
||||||
self.utxos.sort_by(|a, b| a.amount().cmp(&b.amount()).reverse());
|
self.utxos.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse());
|
||||||
|
|
||||||
// We always want to aggregate our UTXOs into a single UTXO in the name of simplicity
|
// We always want to aggregate our UTXOs into a single UTXO in the name of simplicity
|
||||||
// We may have more UTXOs than will fit into a TX though
|
// We may have more UTXOs than will fit into a TX though
|
||||||
|
@ -333,7 +363,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to use all possible UTXOs for all possible payments
|
// We want to use all possible UTXOs for all possible payments
|
||||||
let mut balance = utxos.iter().map(Output::amount).sum::<u64>();
|
let mut balance = utxos.iter().map(|output| output.balance().amount.0).sum::<u64>();
|
||||||
|
|
||||||
// If we can't fulfill the next payment, we have encountered an instance of the UTXO
|
// If we can't fulfill the next payment, we have encountered an instance of the UTXO
|
||||||
// availability problem
|
// availability problem
|
||||||
|
@ -345,7 +375,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
// granting us access to our full balance
|
// granting us access to our full balance
|
||||||
let mut executing = vec![];
|
let mut executing = vec![];
|
||||||
while !self.payments.is_empty() {
|
while !self.payments.is_empty() {
|
||||||
let amount = self.payments[0].amount;
|
let amount = self.payments[0].balance.amount.0;
|
||||||
if balance.checked_sub(amount).is_some() {
|
if balance.checked_sub(amount).is_some() {
|
||||||
balance -= amount;
|
balance -= amount;
|
||||||
executing.push(self.payments.pop_front().unwrap());
|
executing.push(self.payments.pop_front().unwrap());
|
||||||
|
@ -420,7 +450,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
// Get the payments this output is expected to handle
|
// Get the payments this output is expected to handle
|
||||||
let queued = self.queued_plans.get_mut(&expected).unwrap();
|
let queued = self.queued_plans.get_mut(&expected).unwrap();
|
||||||
let mut payments = queued.pop_front().unwrap();
|
let mut payments = queued.pop_front().unwrap();
|
||||||
assert_eq!(expected, payments.iter().map(|payment| payment.amount).sum::<u64>());
|
assert_eq!(expected, payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
||||||
// If this was the last set of payments at this amount, remove it
|
// If this was the last set of payments at this amount, remove it
|
||||||
if queued.is_empty() {
|
if queued.is_empty() {
|
||||||
self.queued_plans.remove(&expected);
|
self.queued_plans.remove(&expected);
|
||||||
|
@ -436,7 +466,7 @@ impl<N: Network> Scheduler<N> {
|
||||||
{
|
{
|
||||||
let mut to_amortize = actual - expected;
|
let mut to_amortize = actual - expected;
|
||||||
// If the payments are worth less than this fee we need to amortize, return, dropping them
|
// If the payments are worth less than this fee we need to amortize, return, dropping them
|
||||||
if payments.iter().map(|payment| payment.amount).sum::<u64>() < to_amortize {
|
if payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>() < to_amortize {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while to_amortize != 0 {
|
while to_amortize != 0 {
|
||||||
|
@ -449,18 +479,20 @@ impl<N: Network> Scheduler<N> {
|
||||||
// Only subtract the overage once
|
// Only subtract the overage once
|
||||||
overage = 0;
|
overage = 0;
|
||||||
|
|
||||||
let subtractable = payment.amount.min(to_subtract);
|
let subtractable = payment.balance.amount.0.min(to_subtract);
|
||||||
to_amortize -= subtractable;
|
to_amortize -= subtractable;
|
||||||
payment.amount -= subtractable;
|
payment.balance.amount.0 -= subtractable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop payments now below the dust threshold
|
// Drop payments now below the dust threshold
|
||||||
let payments =
|
let payments = payments
|
||||||
payments.into_iter().filter(|payment| payment.amount >= N::DUST).collect::<Vec<_>>();
|
.into_iter()
|
||||||
|
.filter(|payment| payment.balance.amount.0 >= N::DUST)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
// Sanity check this was done properly
|
// Sanity check this was done properly
|
||||||
assert!(actual >= payments.iter().map(|payment| payment.amount).sum::<u64>());
|
assert!(actual >= payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
||||||
|
|
||||||
// If there's no payments left, return
|
// If there's no payments left, return
|
||||||
if payments.is_empty() {
|
if payments.is_empty() {
|
||||||
|
|
|
@ -43,7 +43,7 @@ use bitcoin_serai::bitcoin::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{MAX_DATA_LEN, Coin as SeraiCoin, NetworkId, Amount, Balance},
|
primitives::{MAX_DATA_LEN, Coin, NetworkId, Amount, Balance},
|
||||||
networks::bitcoin::Address,
|
networks::bitcoin::Address,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ impl OutputTrait<Bitcoin> for Output {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn balance(&self) -> Balance {
|
fn balance(&self) -> Balance {
|
||||||
Balance { coin: SeraiCoin::Bitcoin, amount: Amount(self.output.value()) }
|
Balance { coin: Coin::Bitcoin, amount: Amount(self.output.value()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data(&self) -> &[u8] {
|
fn data(&self) -> &[u8] {
|
||||||
|
@ -384,6 +384,10 @@ impl Bitcoin {
|
||||||
change: &Option<Address>,
|
change: &Option<Address>,
|
||||||
calculating_fee: bool,
|
calculating_fee: bool,
|
||||||
) -> Result<Option<BSignableTransaction>, NetworkError> {
|
) -> Result<Option<BSignableTransaction>, NetworkError> {
|
||||||
|
for payment in payments {
|
||||||
|
assert_eq!(payment.balance.coin, Coin::Bitcoin);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO2: Use an fee representative of several blocks, cached inside Self
|
// TODO2: Use an fee representative of several blocks, cached inside Self
|
||||||
let block_for_fee = self.get_block(block_number).await?;
|
let block_for_fee = self.get_block(block_number).await?;
|
||||||
let fee = self.median_fee(&block_for_fee).await?;
|
let fee = self.median_fee(&block_for_fee).await?;
|
||||||
|
@ -396,7 +400,7 @@ impl Bitcoin {
|
||||||
// If we're solely estimating the fee, don't specify the actual amount
|
// If we're solely estimating the fee, don't specify the actual amount
|
||||||
// This won't affect the fee calculation yet will ensure we don't hit a not enough funds
|
// This won't affect the fee calculation yet will ensure we don't hit a not enough funds
|
||||||
// error
|
// error
|
||||||
if calculating_fee { Self::DUST } else { payment.amount },
|
if calculating_fee { Self::DUST } else { payment.balance.amount.0 },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
|
@ -106,10 +106,6 @@ pub trait Output<N: Network>: Send + Sync + Sized + Clone + PartialEq + Eq + Deb
|
||||||
fn presumed_origin(&self) -> Option<N::Address>;
|
fn presumed_origin(&self) -> Option<N::Address>;
|
||||||
|
|
||||||
fn balance(&self) -> Balance;
|
fn balance(&self) -> Balance;
|
||||||
// TODO: Remove this?
|
|
||||||
fn amount(&self) -> u64 {
|
|
||||||
self.balance().amount.0
|
|
||||||
}
|
|
||||||
fn data(&self) -> &[u8];
|
fn data(&self) -> &[u8];
|
||||||
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
|
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
|
||||||
|
@ -208,7 +204,7 @@ fn drop_branches<N: Network>(
|
||||||
let mut branch_outputs = vec![];
|
let mut branch_outputs = vec![];
|
||||||
for payment in payments {
|
for payment in payments {
|
||||||
if payment.address == N::branch_address(key) {
|
if payment.address == N::branch_address(key) {
|
||||||
branch_outputs.push(PostFeeBranch { expected: payment.amount, actual: None });
|
branch_outputs.push(PostFeeBranch { expected: payment.balance.amount.0, actual: None });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
branch_outputs
|
branch_outputs
|
||||||
|
@ -279,6 +275,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
/// For any received output, there's the cost to spend the output. This value MUST exceed the
|
/// For any received output, there's the cost to spend the output. This value MUST exceed the
|
||||||
/// cost to spend said output, and should by a notable margin (not just 2x, yet an order of
|
/// cost to spend said output, and should by a notable margin (not just 2x, yet an order of
|
||||||
/// magnitude).
|
/// magnitude).
|
||||||
|
// TODO: Dust needs to be diversified per Coin
|
||||||
const DUST: u64;
|
const DUST: u64;
|
||||||
|
|
||||||
/// The cost to perform input aggregation with a 2-input 1-output TX.
|
/// The cost to perform input aggregation with a 2-input 1-output TX.
|
||||||
|
@ -356,8 +353,9 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
|
|
||||||
let plan_id = plan.id();
|
let plan_id = plan.id();
|
||||||
let Plan { key, inputs, mut payments, change } = plan;
|
let Plan { key, inputs, mut payments, change } = plan;
|
||||||
let theoretical_change_amount = inputs.iter().map(|input| input.amount()).sum::<u64>() -
|
let theoretical_change_amount =
|
||||||
payments.iter().map(|payment| payment.amount).sum::<u64>();
|
inputs.iter().map(|input| input.balance().amount.0).sum::<u64>() -
|
||||||
|
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>();
|
||||||
|
|
||||||
let Some(tx_fee) = self.needed_fee(block_number, &plan_id, &inputs, &payments, &change).await?
|
let Some(tx_fee) = self.needed_fee(block_number, &plan_id, &inputs, &payments, &change).await?
|
||||||
else {
|
else {
|
||||||
|
@ -381,7 +379,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
// as well
|
// as well
|
||||||
let total_fee = tx_fee + if change.is_some() { operating_costs } else { 0 };
|
let total_fee = tx_fee + if change.is_some() { operating_costs } else { 0 };
|
||||||
|
|
||||||
let original_outputs = payments.iter().map(|payment| payment.amount).sum::<u64>();
|
let original_outputs = payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>();
|
||||||
// If this isn't enough for the total fee, drop and move on
|
// If this isn't enough for the total fee, drop and move on
|
||||||
if original_outputs < total_fee {
|
if original_outputs < total_fee {
|
||||||
let mut remaining_operating_costs = operating_costs;
|
let mut remaining_operating_costs = operating_costs;
|
||||||
|
@ -395,7 +393,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
}
|
}
|
||||||
|
|
||||||
let initial_payment_amounts =
|
let initial_payment_amounts =
|
||||||
payments.iter().map(|payment| payment.amount).collect::<Vec<_>>();
|
payments.iter().map(|payment| payment.balance.amount.0).collect::<Vec<_>>();
|
||||||
|
|
||||||
// Amortize the transaction fee across outputs
|
// Amortize the transaction fee across outputs
|
||||||
let mut remaining_fee = total_fee;
|
let mut remaining_fee = total_fee;
|
||||||
|
@ -409,16 +407,16 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
// Only subtract the overage once
|
// Only subtract the overage once
|
||||||
overage = 0;
|
overage = 0;
|
||||||
|
|
||||||
let subtractable = payment.amount.min(this_payment_fee);
|
let subtractable = payment.balance.amount.0.min(this_payment_fee);
|
||||||
remaining_fee -= subtractable;
|
remaining_fee -= subtractable;
|
||||||
payment.amount -= subtractable;
|
payment.balance.amount.0 -= subtractable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any payment is now below the dust threshold, set its value to 0 so it'll be dropped
|
// If any payment is now below the dust threshold, set its value to 0 so it'll be dropped
|
||||||
for payment in &mut payments {
|
for payment in &mut payments {
|
||||||
if payment.amount < Self::DUST {
|
if payment.balance.amount.0 < Self::DUST {
|
||||||
payment.amount = 0;
|
payment.balance.amount.0 = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,7 +426,11 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
if payment.address == Self::branch_address(key) {
|
if payment.address == Self::branch_address(key) {
|
||||||
branch_outputs.push(PostFeeBranch {
|
branch_outputs.push(PostFeeBranch {
|
||||||
expected: initial_amount,
|
expected: initial_amount,
|
||||||
actual: if payment.amount == 0 { None } else { Some(payment.amount) },
|
actual: if payment.balance.amount.0 == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(payment.balance.amount.0)
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -437,7 +439,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
payments = payments
|
payments = payments
|
||||||
.drain(..)
|
.drain(..)
|
||||||
.filter(|payment| {
|
.filter(|payment| {
|
||||||
if payment.amount != 0 {
|
if payment.balance.amount.0 != 0 {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
log::debug!("dropping dust payment from plan {}", hex::encode(plan_id));
|
log::debug!("dropping dust payment from plan {}", hex::encode(plan_id));
|
||||||
|
@ -447,7 +449,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sanity check the fee was successfully amortized
|
// Sanity check the fee was successfully amortized
|
||||||
let new_outputs = payments.iter().map(|payment| payment.amount).sum::<u64>();
|
let new_outputs = payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>();
|
||||||
assert!((new_outputs + total_fee) <= original_outputs);
|
assert!((new_outputs + total_fee) <= original_outputs);
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -483,8 +485,9 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||||
};
|
};
|
||||||
|
|
||||||
if change.is_some() {
|
if change.is_some() {
|
||||||
let on_chain_expected_change = inputs.iter().map(|input| input.amount()).sum::<u64>() -
|
let on_chain_expected_change =
|
||||||
payments.iter().map(|payment| payment.amount).sum::<u64>() -
|
inputs.iter().map(|input| input.balance().amount.0).sum::<u64>() -
|
||||||
|
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>() -
|
||||||
tx_fee;
|
tx_fee;
|
||||||
// If the change value is less than the dust threshold, it becomes an operating cost
|
// If the change value is less than the dust threshold, it becomes an operating cost
|
||||||
// This may be slightly inaccurate as dropping payments may reduce the fee, raising the
|
// This may be slightly inaccurate as dropping payments may reduce the fee, raising the
|
||||||
|
|
|
@ -30,7 +30,7 @@ use monero_serai::{
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
pub use serai_client::{
|
pub use serai_client::{
|
||||||
primitives::{MAX_DATA_LEN, Coin as SeraiCoin, NetworkId, Amount, Balance},
|
primitives::{MAX_DATA_LEN, Coin, NetworkId, Amount, Balance},
|
||||||
networks::monero::Address,
|
networks::monero::Address,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ impl OutputTrait<Monero> for Output {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn balance(&self) -> Balance {
|
fn balance(&self) -> Balance {
|
||||||
Balance { coin: SeraiCoin::Monero, amount: Amount(self.0.commitment().amount) }
|
Balance { coin: Coin::Monero, amount: Amount(self.0.commitment().amount) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn data(&self) -> &[u8] {
|
fn data(&self) -> &[u8] {
|
||||||
|
@ -255,6 +255,10 @@ impl Monero {
|
||||||
change: &Option<Address>,
|
change: &Option<Address>,
|
||||||
calculating_fee: bool,
|
calculating_fee: bool,
|
||||||
) -> Result<Option<(RecommendedTranscript, MSignableTransaction)>, NetworkError> {
|
) -> Result<Option<(RecommendedTranscript, MSignableTransaction)>, NetworkError> {
|
||||||
|
for payment in payments {
|
||||||
|
assert_eq!(payment.balance.coin, Coin::Monero);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO2: Use an fee representative of several blocks, cached inside Self
|
// TODO2: Use an fee representative of several blocks, cached inside Self
|
||||||
let block_for_fee = self.get_block(block_number).await?;
|
let block_for_fee = self.get_block(block_number).await?;
|
||||||
let fee_rate = self.median_fee(&block_for_fee).await?;
|
let fee_rate = self.median_fee(&block_for_fee).await?;
|
||||||
|
@ -313,7 +317,7 @@ impl Monero {
|
||||||
.address(MoneroNetwork::Mainnet, AddressSpec::Standard),
|
.address(MoneroNetwork::Mainnet, AddressSpec::Standard),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
amount: 0,
|
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
|
||||||
data: None,
|
data: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -322,7 +326,9 @@ impl Monero {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// If we're solely estimating the fee, don't actually specify an amount
|
// 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
|
// This won't affect the fee calculation yet will ensure we don't hit an out of funds error
|
||||||
.map(|payment| (payment.address.into(), if calculating_fee { 0 } else { payment.amount }))
|
.map(|payment| {
|
||||||
|
(payment.address.into(), if calculating_fee { 0 } else { payment.balance.amount.0 })
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
match MSignableTransaction::new(
|
match MSignableTransaction::new(
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
use scale::{Encode, Decode};
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
use ciphersuite::group::GroupEncoding;
|
use ciphersuite::group::GroupEncoding;
|
||||||
use frost::curve::Ciphersuite;
|
use frost::curve::Ciphersuite;
|
||||||
|
|
||||||
|
use serai_client::primitives::Balance;
|
||||||
|
|
||||||
use crate::networks::{Output, Network};
|
use crate::networks::{Output, Network};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Payment<N: Network> {
|
pub struct Payment<N: Network> {
|
||||||
pub address: N::Address,
|
pub address: N::Address,
|
||||||
pub data: Option<Vec<u8>>,
|
pub data: Option<Vec<u8>>,
|
||||||
// TODO: Balance
|
pub balance: Balance,
|
||||||
pub amount: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: Network> Payment<N> {
|
impl<N: Network> Payment<N> {
|
||||||
|
@ -21,7 +24,8 @@ impl<N: Network> Payment<N> {
|
||||||
if let Some(data) = self.data.as_ref() {
|
if let Some(data) = self.data.as_ref() {
|
||||||
transcript.append_message(b"data", data);
|
transcript.append_message(b"data", data);
|
||||||
}
|
}
|
||||||
transcript.append_message(b"amount", self.amount.to_le_bytes());
|
transcript.append_message(b"coin", self.balance.coin.encode());
|
||||||
|
transcript.append_message(b"amount", self.balance.amount.0.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
|
@ -40,7 +44,7 @@ impl<N: Network> Payment<N> {
|
||||||
writer.write_all(data)?;
|
writer.write_all(data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write_all(&self.amount.to_le_bytes())
|
writer.write_all(&self.balance.encode())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
|
@ -63,11 +67,10 @@ impl<N: Network> Payment<N> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut buf = [0; 8];
|
let balance = Balance::decode(&mut scale::IoReader(reader))
|
||||||
reader.read_exact(&mut buf)?;
|
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid balance"))?;
|
||||||
let amount = u64::from_le_bytes(buf);
|
|
||||||
|
|
||||||
Ok(Payment { address, data, amount })
|
Ok(Payment { address, data, balance })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ use frost::{
|
||||||
|
|
||||||
use serai_db::{DbTxn, Db, MemDb};
|
use serai_db::{DbTxn, Db, MemDb};
|
||||||
|
|
||||||
|
use serai_client::primitives::{NetworkId, Coin, Amount, Balance};
|
||||||
|
|
||||||
use messages::sign::*;
|
use messages::sign::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
Payment, Plan,
|
Payment, Plan,
|
||||||
|
@ -170,7 +172,19 @@ pub async fn test_signer<N: Network>(network: N) {
|
||||||
Plan {
|
Plan {
|
||||||
key,
|
key,
|
||||||
inputs: outputs.clone(),
|
inputs: outputs.clone(),
|
||||||
payments: vec![Payment { address: N::address(key), data: None, amount }],
|
payments: vec![Payment {
|
||||||
|
address: N::address(key),
|
||||||
|
data: None,
|
||||||
|
balance: Balance {
|
||||||
|
coin: match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("test_signer called with Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
amount: Amount(amount),
|
||||||
|
},
|
||||||
|
}],
|
||||||
change: Some(N::change_address(key)),
|
change: Some(N::change_address(key)),
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
|
@ -201,7 +215,7 @@ pub async fn test_signer<N: Network>(network: N) {
|
||||||
// Adjust the amount for the fees
|
// Adjust the amount for the fees
|
||||||
let amount = amount - tx.fee(&network).await;
|
let amount = amount - tx.fee(&network).await;
|
||||||
// Check either output since Monero will randomize its output order
|
// Check either output since Monero will randomize its output order
|
||||||
assert!((outputs[0].amount() == amount) || (outputs[1].amount() == amount));
|
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount));
|
||||||
|
|
||||||
// Check the eventualities pass
|
// Check the eventualities pass
|
||||||
for eventuality in eventualities {
|
for eventuality in eventualities {
|
||||||
|
|
|
@ -8,6 +8,8 @@ use tokio::time::timeout;
|
||||||
|
|
||||||
use serai_db::{DbTxn, Db, MemDb};
|
use serai_db::{DbTxn, Db, MemDb};
|
||||||
|
|
||||||
|
use serai_client::primitives::{NetworkId, Coin, Amount, Balance};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Payment, Plan,
|
Payment, Plan,
|
||||||
networks::{Output, Transaction, Block, Network},
|
networks::{Output, Transaction, Block, Network},
|
||||||
|
@ -64,12 +66,33 @@ pub async fn test_wallet<N: Network>(network: N) {
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
|
||||||
let mut txn = db.txn();
|
let mut txn = db.txn();
|
||||||
let mut scheduler = Scheduler::new::<MemDb>(&mut txn, key);
|
let mut scheduler = Scheduler::new::<MemDb>(
|
||||||
|
&mut txn,
|
||||||
|
key,
|
||||||
|
match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("test_wallet called with Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
);
|
||||||
let amount = 2 * N::DUST;
|
let amount = 2 * N::DUST;
|
||||||
let plans = scheduler.schedule::<MemDb>(
|
let plans = scheduler.schedule::<MemDb>(
|
||||||
&mut txn,
|
&mut txn,
|
||||||
outputs.clone(),
|
outputs.clone(),
|
||||||
vec![Payment { address: N::address(key), data: None, amount }],
|
vec![Payment {
|
||||||
|
address: N::address(key),
|
||||||
|
data: None,
|
||||||
|
balance: Balance {
|
||||||
|
coin: match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("test_wallet called with Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
amount: Amount(amount),
|
||||||
|
},
|
||||||
|
}],
|
||||||
key,
|
key,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -79,7 +102,19 @@ pub async fn test_wallet<N: Network>(network: N) {
|
||||||
vec![Plan {
|
vec![Plan {
|
||||||
key,
|
key,
|
||||||
inputs: outputs.clone(),
|
inputs: outputs.clone(),
|
||||||
payments: vec![Payment { address: N::address(key), data: None, amount }],
|
payments: vec![Payment {
|
||||||
|
address: N::address(key),
|
||||||
|
data: None,
|
||||||
|
balance: Balance {
|
||||||
|
coin: match N::NETWORK {
|
||||||
|
NetworkId::Serai => panic!("test_wallet called with Serai"),
|
||||||
|
NetworkId::Bitcoin => Coin::Bitcoin,
|
||||||
|
NetworkId::Ethereum => Coin::Ether,
|
||||||
|
NetworkId::Monero => Coin::Monero,
|
||||||
|
},
|
||||||
|
amount: Amount(amount),
|
||||||
|
}
|
||||||
|
}],
|
||||||
change: Some(N::change_address(key)),
|
change: Some(N::change_address(key)),
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -113,7 +148,7 @@ pub async fn test_wallet<N: Network>(network: N) {
|
||||||
let outputs = network.get_outputs(&block, key).await;
|
let outputs = network.get_outputs(&block, key).await;
|
||||||
assert_eq!(outputs.len(), 2);
|
assert_eq!(outputs.len(), 2);
|
||||||
let amount = amount - tx.fee(&network).await;
|
let amount = amount - tx.fee(&network).await;
|
||||||
assert!((outputs[0].amount() == amount) || (outputs[1].amount() == amount));
|
assert!((outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount));
|
||||||
|
|
||||||
for eventuality in eventualities {
|
for eventuality in eventualities {
|
||||||
assert!(network.confirm_completion(&eventuality, &tx));
|
assert!(network.confirm_completion(&eventuality, &tx));
|
||||||
|
|
Loading…
Reference in a new issue