Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zip317 add propose command #917

Merged
merged 14 commits into from
Apr 17, 2024
Merged
10 changes: 10 additions & 0 deletions zingolib/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `wallet::keys::is_transparent_address` fn
- `wallet::notes::NoteRecordIdentifier` struct
- `utils` mod
- `utils::txid_from_hex_encoded_str` fn
- `lightclient::LightClient`:
- `do_propose` behind "zip317" feature
- `do_send_proposal` behind "zip317" feature
- `commands`:
- `ProposeCommand` struct and methods behind "zip317" feature
- `QuickSendCommand` struct and methods behind "zip317" feature
- `test_framework` mod

### Changed

- `wallet::keys::is_shielded_address` takes a `&ChainType` instead of a `&ZingoConfig`
- `wallet::transaction_record_map::TransactionRecordMap` -> `wallet::transaction_records_by_id::TransactionRecordsById`
- `commands`:
- `get_commands` added propose and quicksend to entries behind "zip317" feature
- `SendCommand::help` formatting

### Removed

Expand Down
1 change: 1 addition & 0 deletions zingolib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ lightclient-deprecated = []
default = ["embed_params"]
embed_params = []
darkside_tests = []
zip317 = []

[dependencies]
zingoconfig = { path = "../zingoconfig" }
Expand Down
166 changes: 143 additions & 23 deletions zingolib/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::wallet::keys::is_transparent_address;
use crate::wallet::{MemoDownloadOption, Pool};
use crate::{lightclient::LightClient, wallet};
use indoc::indoc;
Expand All @@ -13,8 +12,6 @@ use zcash_client_backend::address::Address;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::transaction::fees::zip317::MINIMUM_FEE;

use self::error::CommandError;

mod error;
mod utils;

Expand Down Expand Up @@ -790,31 +787,30 @@ impl Command for DecryptMessageCommand {
}
}

struct SendCommand {}
impl Command for SendCommand {
#[cfg(feature = "zip317")]
struct ProposeCommand {}
#[cfg(feature = "zip317")]
impl Command for ProposeCommand {
fn help(&self) -> &'static str {
indoc! {r#"
Send ZEC to a given address(es)
Propose a transfer of ZEC to the given address(es) prior to sending.
The fee required to send this transaction will be added to the proposal and displayed to the user.
Usage:
send <address> <amount in zatoshis> "<optional memo>"
OR
send '[{"address":"<address>", "amount":<amount in zatoshis>, "memo":"<optional memo>"}, ...]'

NOTE: The fee required to send this transaction (currently ZEC 0.0001) is additionally deducted from your balance.
propose <address> <amount in zatoshis> "<optional memo>"
OR
propose '[{"address":"<address>", "amount":<amount in zatoshis>, "memo":"<optional memo>"}, ...]'
Example:
send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 "Hello from the command line"
propose ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 "Hello from the command line"
send

"#}
}

fn short_help(&self) -> &'static str {
"Send ZEC to the given address"
"Propose a transfer of ZEC to the given address(es) prior to sending."
}

fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
// Parse the args. There are two argument types.
// 1 - A set of 2(+1 optional) arguments for a single address send representing address, amount, memo?
// 2 - A single argument in the form of a JSON string that is '[{"address":"<address>", "value":<value>, "memo":"<optional memo>"}, ...]'
let send_inputs = match utils::parse_send_args(args) {
Ok(args) => args,
Err(e) => {
Expand All @@ -824,16 +820,69 @@ impl Command for SendCommand {
)
}
};
for send in &send_inputs {
let address = &send.0;
let memo = &send.2;
if memo.is_some() && is_transparent_address(address, &lightclient.config.chain) {
if let Err(e) = utils::check_memo_compatibility(&send_inputs, &lightclient.config().chain) {
return format!(
zancas marked this conversation as resolved.
Show resolved Hide resolved
"Error: {}\nTry 'help send' for correct usage and examples.",
e,
);
};
RT.block_on(async move {
match lightclient
.do_propose(
send_inputs
.iter()
.map(|(address, amount, memo)| (address.as_str(), *amount, memo.clone()))
.collect(),
)
.await {
Ok(proposal) => {
object! { "fee" => proposal.steps().iter().fold(0, |acc, step| acc + u64::from(step.balance().fee_required())) }
}
Err(e) => {
object! { "error" => e }
}
}
.pretty(2)
})
}
}

struct SendCommand {}
impl Command for SendCommand {
fn help(&self) -> &'static str {
indoc! {r#"
Send ZEC to the given address(es).
The 10_000 zat fee required to send this transaction is additionally deducted from your balance.
Usage:
send <address> <amount in zatoshis> "<optional memo>"
OR
send '[{"address":"<address>", "amount":<amount in zatoshis>, "memo":"<optional memo>"}, ...]'
Example:
send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 "Hello from the command line"

"#}
}

fn short_help(&self) -> &'static str {
"Send ZEC to the given address(es)."
}

fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
let send_inputs = match utils::parse_send_args(args) {
Ok(args) => args,
Err(e) => {
return format!(
"Error: {}\nTry 'help send' for correct usage and examples.",
CommandError::IncompatibleMemo,
);
e
)
}
}
};
if let Err(e) = utils::check_memo_compatibility(&send_inputs, &lightclient.config().chain) {
return format!(
"Error: {}\nTry 'help send' for correct usage and examples.",
e,
);
};
RT.block_on(async move {
match lightclient
.do_send(
Expand All @@ -856,6 +905,72 @@ impl Command for SendCommand {
}
}

#[cfg(feature = "zip317")]
struct QuickSendCommand {}
#[cfg(feature = "zip317")]
impl Command for QuickSendCommand {
fn help(&self) -> &'static str {
indoc! {r#"
Send ZEC to the given address(es). Combines `Propose` and `Send` into a single command.
The fee required to send this transaction is additionally deducted from your balance.
Warning:
Transaction(s) will be sent without the user being aware of the fee amount.
Usage:
quicksend <address> <amount in zatoshis> "<optional memo>"
OR
quicksend '[{"address":"<address>", "amount":<amount in zatoshis>, "memo":"<optional memo>"}, ...]'
Example:
quicksend ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 "Hello from the command line"

"#}
}

fn short_help(&self) -> &'static str {
"Send ZEC to the given address(es). Combines `Propose` and `Send` into a single command."
}

fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
let send_inputs = match utils::parse_send_args(args) {
Ok(args) => args,
Err(e) => {
return format!(
"Error: {}\nTry 'help send' for correct usage and examples.",
e
)
}
};
if let Err(e) = utils::check_memo_compatibility(&send_inputs, &lightclient.config().chain) {
return format!(
"Error: {}\nTry 'help send' for correct usage and examples.",
e,
);
};
RT.block_on(async move {
if let Err(e) = lightclient
.do_propose(
send_inputs
.iter()
.map(|(address, amount, memo)| (address.as_str(), *amount, memo.clone()))
.collect(),
)
.await {
return e;
};
match lightclient
.do_send_proposal().await
{
Ok(txids) => {
object! { "txids" => txids.iter().map(|txid| txid.to_string()).collect::<Vec<String>>()}
}
Err(e) => {
object! { "error" => e }
}
}
.pretty(2)
})
}
}

struct DeleteCommand {}
impl Command for DeleteCommand {
fn help(&self) -> &'static str {
Expand Down Expand Up @@ -1434,6 +1549,11 @@ pub fn get_commands() -> HashMap<&'static str, Box<dyn Command>> {
{
entries.push(("list", Box::new(TransactionsCommand {})));
}
#[cfg(feature = "zip317")]
{
entries.push(("propose", Box::new(ProposeCommand {})));
entries.push(("quicksend", Box::new(QuickSendCommand {})));
}
entries.into_iter().collect()
}

Expand Down
65 changes: 62 additions & 3 deletions zingolib/src/commands/utils.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//! Module containing utility functions for the commands interface
// Module containing utility functions for the commands interface

use crate::commands::error::CommandError;
use crate::wallet;
use zcash_primitives::memo::MemoBytes;
use zingoconfig::ChainType;

/// Send args accepts two different formats for its input
// Parse the send arguments for `do_propose`.
// The send arguments have two possible formats:
// - 1 argument in the form of a JSON string for multiple sends. '[{"address":"<address>", "value":<value>, "memo":"<optional memo>"}, ...]'
// - 2 (+1 optional) arguments for a single address send. &["<address>", <amount>, "<optional memo>"]
pub(super) fn parse_send_args(
args: &[&str],
) -> Result<Vec<(String, u64, Option<MemoBytes>)>, CommandError> {
Expand Down Expand Up @@ -76,9 +80,25 @@ pub(super) fn parse_send_args(
Ok(send_args)
}

// Checks send inputs do not contain memo's to transparent addresses.
pub(super) fn check_memo_compatibility(
send_inputs: &[(String, u64, Option<MemoBytes>)],
chain: &ChainType,
) -> Result<(), CommandError> {
for send in send_inputs {
let address = &send.0;
let memo = &send.2;
if memo.is_some() && wallet::keys::is_transparent_address(address, chain) {
return Err(CommandError::IncompatibleMemo);
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use crate::wallet;
use crate::{commands::error::CommandError, wallet};

#[test]
fn parse_send_args() {
Expand Down Expand Up @@ -129,6 +149,7 @@ mod tests {
mod fail_parse_send_args {
mod json_array {
use crate::commands::{error::CommandError, utils::parse_send_args};

#[test]
fn failed_json_parsing() {
let args = [r#"testaddress{{"#];
Expand Down Expand Up @@ -265,4 +286,42 @@ mod tests {
}
}
}

#[test]
fn check_memo_compatibility() {
let value_str = "100000";
let memo_str = "test memo";

// shielded address with memo
let address = "zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p";
let send_inputs = super::parse_send_args(&[address, value_str, memo_str]).unwrap();
super::check_memo_compatibility(
&send_inputs,
&zingoconfig::ChainType::Regtest(zingoconfig::RegtestNetwork::all_upgrades_active()),
)
.unwrap();

// transparent address without memo
let address = "tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd";
let value_str = "100000";
let send_inputs = super::parse_send_args(&[address, value_str]).unwrap();
super::check_memo_compatibility(
&send_inputs,
&zingoconfig::ChainType::Regtest(zingoconfig::RegtestNetwork::all_upgrades_active()),
)
.unwrap();

// transparent address with memo
let address = "tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd";
let value_str = "100000";
let memo_str = "test memo";
let send_inputs = super::parse_send_args(&[address, value_str, memo_str]).unwrap();
match super::check_memo_compatibility(
&send_inputs,
&zingoconfig::ChainType::Regtest(zingoconfig::RegtestNetwork::all_upgrades_active()),
) {
Err(CommandError::IncompatibleMemo) => (),
_ => panic!(),
};
}
}
28 changes: 27 additions & 1 deletion zingolib/src/lightclient/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ use zcash_proofs::prover::LocalTxProver;
use super::{LightClient, LightWalletSendProgress};
use crate::wallet::Pool;

#[cfg(feature = "zip317")]
use {
crate::wallet::notes::NoteRecordIdentifier,
zcash_client_backend::proposal::Proposal,
zcash_primitives::transaction::{fees::zip317::FeeRule, TxId},
};

impl LightClient {
async fn get_submission_height(&self) -> Result<BlockHeight, String> {
Ok(BlockHeight::from_u32(
Expand Down Expand Up @@ -53,7 +60,26 @@ impl LightClient {
.collect()
}

//TODO: Add migrate_sapling_to_orchard argument
/// Unstable function to expose the zip317 interface for development
zancas marked this conversation as resolved.
Show resolved Hide resolved
// TODO: add correct functionality and doc comments / tests
#[cfg(feature = "zip317")]
pub async fn do_propose(
&self,
_address_amount_memo_tuples: Vec<(&str, u64, Option<MemoBytes>)>,
) -> Result<Proposal<FeeRule, NoteRecordIdentifier>, String> {
use crate::test_framework::mocks::ProposalBuilder;

Ok(ProposalBuilder::default().build())
}

/// Unstable function to expose the zip317 interface for development
// TODO: add correct functionality and doc comments / tests
#[cfg(feature = "zip317")]
pub async fn do_send_proposal(&self) -> Result<Vec<TxId>, String> {
Ok(vec![TxId::from_bytes([0u8; 32])])
zancas marked this conversation as resolved.
Show resolved Hide resolved
}

// TODO: Add migrate_sapling_to_orchard argument
pub async fn do_send(
&self,
address_amount_memo_tuples: Vec<(&str, u64, Option<MemoBytes>)>,
Expand Down
Loading