mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-18 16:55:16 +00:00
monero-serai: make it clear that not providing a change address is fingerprintable (#472)
* Make it clear not providing a change address is fingerprintable When no change address is provided, all change is shunted to the fee. This PR makes it clear to the caller that it is fingerprintable when the caller does this. * Review comments
This commit is contained in:
parent
16b22dd105
commit
397fca748f
8 changed files with 91 additions and 44 deletions
|
@ -18,14 +18,14 @@ struct SignableTransactionBuilderInternal {
|
||||||
r_seed: Option<Zeroizing<[u8; 32]>>,
|
r_seed: Option<Zeroizing<[u8; 32]>>,
|
||||||
inputs: Vec<(SpendableOutput, Decoys)>,
|
inputs: Vec<(SpendableOutput, Decoys)>,
|
||||||
payments: Vec<(MoneroAddress, u64)>,
|
payments: Vec<(MoneroAddress, u64)>,
|
||||||
change_address: Option<Change>,
|
change_address: Change,
|
||||||
data: Vec<Vec<u8>>,
|
data: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignableTransactionBuilderInternal {
|
impl SignableTransactionBuilderInternal {
|
||||||
// Takes in the change address so users don't miss that they have to manually set one
|
// Takes in the change address so users don't miss that they have to manually set one
|
||||||
// If they don't, all leftover funds will become part of the fee
|
// If they don't, all leftover funds will become part of the fee
|
||||||
fn new(protocol: Protocol, fee_rate: Fee, change_address: Option<Change>) -> Self {
|
fn new(protocol: Protocol, fee_rate: Fee, change_address: Change) -> Self {
|
||||||
Self {
|
Self {
|
||||||
protocol,
|
protocol,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
|
@ -90,7 +90,7 @@ impl SignableTransactionBuilder {
|
||||||
Self(self.0.clone())
|
Self(self.0.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(protocol: Protocol, fee_rate: Fee, change_address: Option<Change>) -> Self {
|
pub fn new(protocol: Protocol, fee_rate: Fee, change_address: Change) -> Self {
|
||||||
Self(Arc::new(RwLock::new(SignableTransactionBuilderInternal::new(
|
Self(Arc::new(RwLock::new(SignableTransactionBuilderInternal::new(
|
||||||
protocol,
|
protocol,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
|
|
|
@ -292,7 +292,7 @@ impl FeePriority {
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub(crate) enum InternalPayment {
|
pub(crate) enum InternalPayment {
|
||||||
Payment((MoneroAddress, u64)),
|
Payment((MoneroAddress, u64)),
|
||||||
Change(Change, u64),
|
Change((MoneroAddress, Option<Zeroizing<Scalar>>), u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The eventual output of a SignableTransaction.
|
/// The eventual output of a SignableTransaction.
|
||||||
|
@ -324,7 +324,7 @@ pub struct SignableTransaction {
|
||||||
/// Specification for a change output.
|
/// Specification for a change output.
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||||||
pub struct Change {
|
pub struct Change {
|
||||||
address: MoneroAddress,
|
address: Option<MoneroAddress>,
|
||||||
view: Option<Zeroizing<Scalar>>,
|
view: Option<Zeroizing<Scalar>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,21 +338,21 @@ impl Change {
|
||||||
/// Create a change output specification from a ViewPair, as needed to maintain privacy.
|
/// Create a change output specification from a ViewPair, as needed to maintain privacy.
|
||||||
pub fn new(view: &ViewPair, guaranteed: bool) -> Change {
|
pub fn new(view: &ViewPair, guaranteed: bool) -> Change {
|
||||||
Change {
|
Change {
|
||||||
address: view.address(
|
address: Some(view.address(
|
||||||
Network::Mainnet,
|
Network::Mainnet,
|
||||||
if !guaranteed {
|
if !guaranteed {
|
||||||
AddressSpec::Standard
|
AddressSpec::Standard
|
||||||
} else {
|
} else {
|
||||||
AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true }
|
AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true }
|
||||||
},
|
},
|
||||||
),
|
)),
|
||||||
view: Some(view.view.clone()),
|
view: Some(view.view.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a fingerprintable change output specification which will harm privacy. Only use this
|
/// Create a fingerprintable change output specification which will harm privacy. Only use this
|
||||||
/// if you know what you're doing.
|
/// if you know what you're doing.
|
||||||
pub fn fingerprintable(address: MoneroAddress) -> Change {
|
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
|
||||||
Change { address, view: None }
|
Change { address, view: None }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,13 +364,13 @@ fn need_additional(payments: &[InternalPayment]) -> (bool, bool) {
|
||||||
.filter(|payment| match *payment {
|
.filter(|payment| match *payment {
|
||||||
InternalPayment::Payment(payment) => payment.0.is_subaddress(),
|
InternalPayment::Payment(payment) => payment.0.is_subaddress(),
|
||||||
InternalPayment::Change(change, _) => {
|
InternalPayment::Change(change, _) => {
|
||||||
if change.view.is_some() {
|
if change.1.is_some() {
|
||||||
has_change_view = true;
|
has_change_view = true;
|
||||||
// It should not be possible to construct a change specification to a subaddress with a
|
// It should not be possible to construct a change specification to a subaddress with a
|
||||||
// view key
|
// view key
|
||||||
debug_assert!(!change.address.is_subaddress());
|
debug_assert!(!change.0.is_subaddress());
|
||||||
}
|
}
|
||||||
change.address.is_subaddress()
|
change.0.is_subaddress()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.count() !=
|
.count() !=
|
||||||
|
@ -415,7 +415,7 @@ impl SignableTransaction {
|
||||||
r_seed: Option<Zeroizing<[u8; 32]>>,
|
r_seed: Option<Zeroizing<[u8; 32]>>,
|
||||||
inputs: Vec<(SpendableOutput, Decoys)>,
|
inputs: Vec<(SpendableOutput, Decoys)>,
|
||||||
payments: Vec<(MoneroAddress, u64)>,
|
payments: Vec<(MoneroAddress, u64)>,
|
||||||
change_address: Option<Change>,
|
change: Change,
|
||||||
data: Vec<Vec<u8>>,
|
data: Vec<Vec<u8>>,
|
||||||
fee_rate: Fee,
|
fee_rate: Fee,
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
) -> Result<SignableTransaction, TransactionError> {
|
||||||
|
@ -430,8 +430,8 @@ impl SignableTransaction {
|
||||||
for payment in &payments {
|
for payment in &payments {
|
||||||
count(payment.0);
|
count(payment.0);
|
||||||
}
|
}
|
||||||
if let Some(change) = change_address.as_ref() {
|
if let Some(change_address) = change.address.as_ref() {
|
||||||
count(change.address);
|
count(*change_address);
|
||||||
}
|
}
|
||||||
if payment_ids > 1 {
|
if payment_ids > 1 {
|
||||||
Err(TransactionError::MultiplePaymentIds)?;
|
Err(TransactionError::MultiplePaymentIds)?;
|
||||||
|
@ -459,24 +459,24 @@ impl SignableTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have two outputs, as required by Monero, error
|
// If we don't have two outputs, as required by Monero, error
|
||||||
if (payments.len() == 1) && change_address.is_none() {
|
if (payments.len() == 1) && change.address.is_none() {
|
||||||
Err(TransactionError::NoChange)?;
|
Err(TransactionError::NoChange)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the outgoing amount ignoring fees
|
// Get the outgoing amount ignoring fees
|
||||||
let out_amount = payments.iter().map(|payment| payment.1).sum::<u64>();
|
let out_amount = payments.iter().map(|payment| payment.1).sum::<u64>();
|
||||||
|
|
||||||
let outputs = payments.len() + usize::from(change_address.is_some());
|
let outputs = payments.len() + usize::from(change.address.is_some());
|
||||||
if outputs > MAX_OUTPUTS {
|
if outputs > MAX_OUTPUTS {
|
||||||
Err(TransactionError::TooManyOutputs)?;
|
Err(TransactionError::TooManyOutputs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect payments in a container that includes a change output if a change address is provided
|
// Collect payments in a container that includes a change output if a change address is provided
|
||||||
let mut payments = payments.into_iter().map(InternalPayment::Payment).collect::<Vec<_>>();
|
let mut payments = payments.into_iter().map(InternalPayment::Payment).collect::<Vec<_>>();
|
||||||
if change_address.is_some() {
|
if let Some(change_address) = change.address.as_ref() {
|
||||||
// Push a 0 amount change output that we'll use to do fee calculations.
|
// Push a 0 amount change output that we'll use to do fee calculations.
|
||||||
// We'll modify the change amount after calculating the fee
|
// We'll modify the change amount after calculating the fee
|
||||||
payments.push(InternalPayment::Change(change_address.clone().unwrap(), 0));
|
payments.push(InternalPayment::Change((*change_address, change.view.clone()), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if we'll need additional pub keys in tx extra
|
// Determine if we'll need additional pub keys in tx extra
|
||||||
|
@ -517,21 +517,23 @@ impl SignableTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check we have the expected number of change outputs
|
// Sanity check we have the expected number of change outputs
|
||||||
sanity_check_change_payment_quantity(&payments, change_address.is_some());
|
sanity_check_change_payment_quantity(&payments, change.address.is_some());
|
||||||
|
|
||||||
// Modify the amount of the change output
|
// Modify the amount of the change output
|
||||||
if change_address.is_some() {
|
if let Some(change_address) = change.address.as_ref() {
|
||||||
let change_payment = payments.last_mut().unwrap();
|
let change_payment = payments.last_mut().unwrap();
|
||||||
debug_assert!(matches!(change_payment, InternalPayment::Change(_, _)));
|
debug_assert!(matches!(change_payment, InternalPayment::Change(_, _)));
|
||||||
*change_payment =
|
*change_payment = InternalPayment::Change(
|
||||||
InternalPayment::Change(change_address.clone().unwrap(), in_amount - out_amount - fee);
|
(*change_address, change.view.clone()),
|
||||||
|
in_amount - out_amount - fee,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check the change again after modifying
|
// Sanity check the change again after modifying
|
||||||
sanity_check_change_payment_quantity(&payments, change_address.is_some());
|
sanity_check_change_payment_quantity(&payments, change.address.is_some());
|
||||||
|
|
||||||
// Sanity check outgoing amount + fee == incoming amount
|
// Sanity check outgoing amount + fee == incoming amount
|
||||||
if change_address.is_some() {
|
if change.address.is_some() {
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
payments
|
payments
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -551,7 +553,7 @@ impl SignableTransaction {
|
||||||
r_seed,
|
r_seed,
|
||||||
inputs,
|
inputs,
|
||||||
payments,
|
payments,
|
||||||
has_change: change_address.is_some(),
|
has_change: change.address.is_some(),
|
||||||
data,
|
data,
|
||||||
fee,
|
fee,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
|
@ -624,7 +626,7 @@ impl SignableTransaction {
|
||||||
// regarding it's view key
|
// regarding it's view key
|
||||||
payment = if !modified_change_ecdh {
|
payment = if !modified_change_ecdh {
|
||||||
if let InternalPayment::Change(change, amount) = &payment {
|
if let InternalPayment::Change(change, amount) = &payment {
|
||||||
InternalPayment::Payment((change.address, *amount))
|
InternalPayment::Payment((change.0, *amount))
|
||||||
} else {
|
} else {
|
||||||
payment
|
payment
|
||||||
}
|
}
|
||||||
|
@ -656,8 +658,8 @@ impl SignableTransaction {
|
||||||
InternalPayment::Change(change, amount) => {
|
InternalPayment::Change(change, amount) => {
|
||||||
// Instead of rA, use Ra, where R is r * subaddress_spend_key
|
// Instead of rA, use Ra, where R is r * subaddress_spend_key
|
||||||
// change.view must be Some as if it's None, this payment would've been downcast
|
// change.view must be Some as if it's None, this payment would've been downcast
|
||||||
let ecdh = tx_public_key * change.view.unwrap().deref();
|
let ecdh = tx_public_key * change.1.unwrap().deref();
|
||||||
SendOutput::change(ecdh, uniqueness, (o, (change.address, amount)))
|
SendOutput::change(ecdh, uniqueness, (o, (change.0, amount)))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -954,8 +956,8 @@ impl Eventuality {
|
||||||
}
|
}
|
||||||
InternalPayment::Change(change, amount) => {
|
InternalPayment::Change(change, amount) => {
|
||||||
w.write_all(&[1])?;
|
w.write_all(&[1])?;
|
||||||
write_vec(write_byte, change.address.to_string().as_bytes(), w)?;
|
write_vec(write_byte, change.0.to_string().as_bytes(), w)?;
|
||||||
if let Some(view) = change.view.as_ref() {
|
if let Some(view) = change.1.as_ref() {
|
||||||
w.write_all(&[1])?;
|
w.write_all(&[1])?;
|
||||||
write_scalar(view, w)?;
|
write_scalar(view, w)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -988,14 +990,14 @@ impl Eventuality {
|
||||||
Ok(match read_byte(r)? {
|
Ok(match read_byte(r)? {
|
||||||
0 => InternalPayment::Payment((read_address(r)?, read_u64(r)?)),
|
0 => InternalPayment::Payment((read_address(r)?, read_u64(r)?)),
|
||||||
1 => InternalPayment::Change(
|
1 => InternalPayment::Change(
|
||||||
Change {
|
(
|
||||||
address: read_address(r)?,
|
read_address(r)?,
|
||||||
view: match read_byte(r)? {
|
match read_byte(r)? {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(Zeroizing::new(read_scalar(r)?)),
|
1 => Some(Zeroizing::new(read_scalar(r)?)),
|
||||||
_ => Err(io::Error::other("invalid change payment"))?,
|
_ => Err(io::Error::other("invalid change payment"))?,
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
read_u64(r)?,
|
read_u64(r)?,
|
||||||
),
|
),
|
||||||
_ => Err(io::Error::other("invalid payment"))?,
|
_ => Err(io::Error::other("invalid payment"))?,
|
||||||
|
|
|
@ -125,8 +125,8 @@ impl SignableTransaction {
|
||||||
transcript.append_message(b"payment_amount", payment.1.to_le_bytes());
|
transcript.append_message(b"payment_amount", payment.1.to_le_bytes());
|
||||||
}
|
}
|
||||||
InternalPayment::Change(change, amount) => {
|
InternalPayment::Change(change, amount) => {
|
||||||
transcript.append_message(b"change_address", change.address.to_string().as_bytes());
|
transcript.append_message(b"change_address", change.0.to_string().as_bytes());
|
||||||
if let Some(view) = change.view.as_ref() {
|
if let Some(view) = change.1.as_ref() {
|
||||||
transcript.append_message(b"change_view_key", Zeroizing::new(view.to_bytes()));
|
transcript.append_message(b"change_view_key", Zeroizing::new(view.to_bytes()));
|
||||||
}
|
}
|
||||||
transcript.append_message(b"change_amount", amount.to_le_bytes());
|
transcript.append_message(b"change_amount", amount.to_le_bytes());
|
||||||
|
|
|
@ -213,13 +213,13 @@ macro_rules! test {
|
||||||
let builder = SignableTransactionBuilder::new(
|
let builder = SignableTransactionBuilder::new(
|
||||||
protocol,
|
protocol,
|
||||||
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
||||||
Some(Change::new(
|
Change::new(
|
||||||
&ViewPair::new(
|
&ViewPair::new(
|
||||||
&random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
&random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
||||||
Zeroizing::new(random_scalar(&mut OsRng))
|
Zeroizing::new(random_scalar(&mut OsRng))
|
||||||
),
|
),
|
||||||
false
|
false
|
||||||
)),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let sign = |tx: SignableTransaction| {
|
let sign = |tx: SignableTransaction| {
|
||||||
|
@ -298,7 +298,11 @@ macro_rules! test {
|
||||||
rpc.publish_transaction(&signed).await.unwrap();
|
rpc.publish_transaction(&signed).await.unwrap();
|
||||||
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
||||||
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
||||||
check_weight_and_fee(&tx, fee_rate);
|
if stringify!($name) != "spend_one_input_to_two_outputs_no_change" {
|
||||||
|
// Skip weight and fee check for the above test because when there is no change,
|
||||||
|
// the change is added to the fee
|
||||||
|
check_weight_and_fee(&tx, fee_rate);
|
||||||
|
}
|
||||||
#[allow(unused_assignments)]
|
#[allow(unused_assignments)]
|
||||||
{
|
{
|
||||||
let scanner =
|
let scanner =
|
||||||
|
|
|
@ -111,7 +111,7 @@ test!(
|
||||||
let mut builder = SignableTransactionBuilder::new(
|
let mut builder = SignableTransactionBuilder::new(
|
||||||
protocol,
|
protocol,
|
||||||
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
||||||
Some(Change::new(&change_view, false)),
|
Change::new(&change_view, false),
|
||||||
);
|
);
|
||||||
add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
|
add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
|
||||||
|
|
||||||
|
@ -273,3 +273,44 @@ test!(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
spend_one_input_to_two_outputs_no_change,
|
||||||
|
(
|
||||||
|
|_, mut builder: Builder, addr| async move {
|
||||||
|
builder.add_payment(addr, 1000000000000);
|
||||||
|
(builder.build().unwrap(), ())
|
||||||
|
},
|
||||||
|
|_, tx: Transaction, mut scanner: Scanner, _| async move {
|
||||||
|
let mut outputs = scanner.scan_transaction(&tx).not_locked();
|
||||||
|
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|
||||||
|
assert_eq!(outputs[0].commitment().amount, 1000000000000);
|
||||||
|
outputs
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
|protocol, rpc: Rpc<_>, _, addr, outputs: Vec<ReceivedOutput>| async move {
|
||||||
|
use monero_serai::wallet::FeePriority;
|
||||||
|
|
||||||
|
let mut builder = SignableTransactionBuilder::new(
|
||||||
|
protocol,
|
||||||
|
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
||||||
|
Change::fingerprintable(None),
|
||||||
|
);
|
||||||
|
add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
|
||||||
|
builder.add_payment(addr, 10000);
|
||||||
|
builder.add_payment(addr, 50000);
|
||||||
|
|
||||||
|
(builder.build().unwrap(), ())
|
||||||
|
},
|
||||||
|
|_, tx: Transaction, mut scanner: Scanner, _| async move {
|
||||||
|
let mut outputs = scanner.scan_transaction(&tx).not_locked();
|
||||||
|
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
|
||||||
|
assert_eq!(outputs[0].commitment().amount, 10000);
|
||||||
|
assert_eq!(outputs[1].commitment().amount, 50000);
|
||||||
|
|
||||||
|
// The remainder should get shunted to fee, which is fingerprintable
|
||||||
|
assert_eq!(tx.rct_signatures.base.fee, 1000000000000 - 10000 - 50000);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
|
@ -384,7 +384,7 @@ impl Monero {
|
||||||
Some(Zeroizing::new(*plan_id)),
|
Some(Zeroizing::new(*plan_id)),
|
||||||
inputs.clone(),
|
inputs.clone(),
|
||||||
payments,
|
payments,
|
||||||
change.clone().map(|change| Change::fingerprintable(change.into())),
|
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
|
||||||
vec![],
|
vec![],
|
||||||
fee_rate,
|
fee_rate,
|
||||||
) {
|
) {
|
||||||
|
@ -753,7 +753,7 @@ impl Network for Monero {
|
||||||
None,
|
None,
|
||||||
inputs,
|
inputs,
|
||||||
vec![(address.into(), amount - fee)],
|
vec![(address.into(), amount - fee)],
|
||||||
Some(Change::fingerprintable(Self::test_address().into())),
|
Change::fingerprintable(Some(Self::test_address().into())),
|
||||||
vec![],
|
vec![],
|
||||||
self.rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
self.rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -395,7 +395,7 @@ async fn mint_and_burn_test() {
|
||||||
),
|
),
|
||||||
1_100_000_000_000,
|
1_100_000_000_000,
|
||||||
)],
|
)],
|
||||||
Some(Change::new(&view_pair, false)),
|
Change::new(&view_pair, false),
|
||||||
vec![Shorthand::transfer(None, serai_addr).encode()],
|
vec![Shorthand::transfer(None, serai_addr).encode()],
|
||||||
rpc.get_fee(Protocol::v16, FeePriority::Low).await.unwrap(),
|
rpc.get_fee(Protocol::v16, FeePriority::Low).await.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -361,7 +361,7 @@ impl Wallet {
|
||||||
None,
|
None,
|
||||||
these_inputs.drain(..).zip(decoys.drain(..)).collect(),
|
these_inputs.drain(..).zip(decoys.drain(..)).collect(),
|
||||||
vec![(to_addr, AMOUNT)],
|
vec![(to_addr, AMOUNT)],
|
||||||
Some(Change::new(view_pair, false)),
|
Change::new(view_pair, false),
|
||||||
data,
|
data,
|
||||||
rpc.get_fee(Protocol::v16, FeePriority::Low).await.unwrap(),
|
rpc.get_fee(Protocol::v16, FeePriority::Low).await.unwrap(),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue