Slatepack address index on send (#821)

* Store address_index into tx log entry to get address and decrypt slatepack message, increment over all values

* Provide address index on send and issue_invoice command

* Keep 0 index for receive

* Use sender address index at payment proof

Rust API users need to update to SlatepackAddressIndex and the Slatepack methods
Keep a wallet database backup for pending transactions and saved address indices. Seed recovery alone does not restore these, and the address counter restarts at 1.
This commit is contained in:
ardocrat
2026-10-13 16:41:29 +03:00
committed by GitHub
parent dce4e5eecc
commit b8ddf13001
31 changed files with 1171 additions and 735 deletions
+10
View File
@@ -185,6 +185,11 @@ subcommands:
- amount_includes_fee:
help: Transaction amount includes transaction fee. Recipient will receive (amount - fee).
long: amount_includes_fee
- address_index:
help: Slatepack address derivation path index
short: a
long: address_index
takes_value: true
- unpack:
about: Unpack and display an armored Slatepack Message, decrypting if possible
args:
@@ -264,6 +269,11 @@ subcommands:
help: Show slatepack data as QR code
short: q
long: slatepack_qr
- address_index:
help: Slatepack address derivation path index
short: a
long: address_index
takes_value: true
- pay:
about: Spend coins to pay the provided invoice transaction
args:
+67 -65
View File
@@ -20,7 +20,6 @@ use crate::util::file::get_first_line;
use crate::util::secp::key::SecretKey;
use crate::util::{Mutex, ZeroingString};
use clap::ArgMatches;
use grin_core as core;
use grin_core::core::amount_to_hr_string;
use grin_keychain as keychain;
@@ -29,8 +28,11 @@ use grin_wallet_config::{GlobalWalletConfig, TorConfig, WalletConfig};
use grin_wallet_controller::command::GlobalArgs;
use grin_wallet_controller::{command, Error};
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl};
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor};
use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, WalletInst, WalletLCProvider};
use clap::ArgMatches;
use linefeed::terminal::Signal;
use linefeed::{Interface, ReadResult};
use rpassword;
@@ -38,6 +40,7 @@ use std::convert::TryFrom;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uuid::Uuid;
// define what to do on argument error
macro_rules! arg_parse {
@@ -526,6 +529,8 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
let slatepack_qr = args.is_present("slatepack_qr");
let address_index = parse_address_index_arg(args)?;
Ok(command::SendArgs {
amount,
amount_includes_fee,
@@ -545,23 +550,32 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
skip_tor,
bridge,
slatepack_qr,
address_index,
})
}
pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, ParseError> {
// input file
let input_file = match args.is_present("input") {
true => {
let file = args.value_of("input").unwrap().to_owned();
// validate input
if !Path::new(&file).is_file() {
let msg = format!("File {} not found.", file);
fn parse_address_index_arg(args: &ArgMatches) -> Result<Option<SlatepackAddressIndex>, ParseError> {
let address = if let Some(i) = parse_optional(args, "address_index")? {
let val = i.parse::<u32>();
match val {
Ok(v) => Some(
SlatepackAddressIndex(v)
.check()
.map_err(|e| ParseError::ArgumentError(e.to_string()))?,
),
Err(e) => {
let msg = format!("Could not parse address_index as a whole number. e={}", e);
return Err(ParseError::ArgumentError(msg));
}
Some(file)
}
false => None,
} else {
None
};
Ok(address)
}
pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, ParseError> {
let input_file = parse_input_file_arg(args)?;
let mut input_slatepack_message = None;
if input_file.is_none() {
@@ -590,8 +604,7 @@ pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Par
})
}
pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, ParseError> {
// input file
fn parse_input_file_arg(args: &ArgMatches) -> Result<Option<String>, ParseError> {
let input_file = match args.is_present("input") {
true => {
let file = args.value_of("input").unwrap().to_owned();
@@ -604,6 +617,11 @@ pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Pars
}
false => None,
};
Ok(input_file)
}
pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, ParseError> {
let input_file = parse_input_file_arg(args)?;
let mut input_slatepack_message = None;
if input_file.is_none() {
@@ -636,18 +654,7 @@ pub fn parse_finalize_args(args: &ArgMatches) -> Result<command::FinalizeArgs, P
let fluff = args.is_present("fluff");
let nopost = args.is_present("nopost");
let input_file = match args.is_present("input") {
true => {
let file = args.value_of("input").unwrap().to_owned();
// validate input
if !Path::new(&file).is_file() {
let msg = format!("File {} not found.", file);
return Err(ParseError::ArgumentError(msg));
}
Some(file)
}
false => None,
};
let input_file = parse_input_file_arg(args)?;
let mut input_slatepack_message = None;
if input_file.is_none() {
@@ -702,12 +709,15 @@ pub fn parse_issue_invoice_args(
let slatepack_qr = args.is_present("slatepack_qr");
let address_index = parse_address_index_arg(args)?;
Ok(command::IssueInvoiceArgs {
dest,
issue_args: IssueInvoiceTxArgs {
dest_acct_name: None,
amount,
target_slate_version,
address_index,
},
outfile,
slatepack_qr,
@@ -757,7 +767,7 @@ pub fn parse_process_invoice_args(
args: &ArgMatches,
prompt: bool,
slate: Slate,
ret_address: Option<SlatepackAddress>,
mut ret_address: Option<SlatepackAddress>,
) -> Result<command::ProcessInvoiceArgs, ParseError> {
// minimum_confirmations
let min_c = parse_required(args, "minimum_confirmations")?;
@@ -776,9 +786,18 @@ pub fn parse_process_invoice_args(
let max_outputs = 500;
if prompt {
let dest = match ret_address.clone() {
Some(a) => String::try_from(&a).unwrap(),
None => String::from(""),
let dest = parse_optional(args, "dest")?;
let dest = match dest {
Some(dest) => {
ret_address = Some(SlatepackAddress::try_from(dest.as_str()).map_err(|e| {
ParseError::ArgumentError(format!("Can not parse destination address {}", e))
})?);
dest
}
None => match ret_address.clone() {
Some(a) => String::try_from(&a).unwrap(),
None => String::from(""),
},
};
// Now we need to prompt the user whether they want to do this,
prompt_pay_invoice(&slate, &dest)?;
@@ -842,16 +861,7 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, ParseError>
None => None,
Some(tx) => Some(parse_u64(tx, "id")? as u32),
};
let tx_slate_id = match args.value_of("txid") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => Some(t),
Err(e) => {
let msg = format!("Could not parse txid parameter. e={}", e);
return Err(ParseError::ArgumentError(msg));
}
},
};
let tx_slate_id = parse_tx_slate_id_arg(args)?;
if tx_id.is_some() && tx_slate_id.is_some() {
let msg = "At most one of 'id' (-i) or 'txid' (-t) may be provided.".to_string();
return Err(ParseError::ArgumentError(msg));
@@ -867,22 +877,24 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, ParseError>
})
}
fn parse_tx_slate_id_arg(args: &ArgMatches) -> Result<Option<Uuid>, ParseError> {
let tx_slate_id = match args.value_of("txid") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => Some(t),
Err(e) => {
let msg = format!("Could not parse txid parameter. e={}", e);
return Err(ParseError::ArgumentError(msg));
}
},
};
Ok(tx_slate_id)
}
pub fn parse_post_args(args: &ArgMatches) -> Result<command::PostArgs, ParseError> {
let fluff = args.is_present("fluff");
// input file
let input_file = match args.is_present("input") {
true => {
let file = args.value_of("input").unwrap().to_owned();
// validate input
if !Path::new(&file).is_file() {
let msg = format!("File {} not found.", file);
return Err(ParseError::ArgumentError(msg));
}
Some(file)
}
false => None,
};
let input_file = parse_input_file_arg(args)?;
let mut input_slatepack_message = None;
if input_file.is_none() {
@@ -948,16 +960,7 @@ pub fn parse_export_proof_args(args: &ArgMatches) -> Result<command::ProofExport
None => None,
Some(tx) => Some(parse_u64(tx, "id")? as u32),
};
let tx_slate_id = match args.value_of("txid") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => Some(t),
Err(e) => {
let msg = format!("Could not parse txid parameter. e={}", e);
return Err(ParseError::ArgumentError(msg));
}
},
};
let tx_slate_id = parse_tx_slate_id_arg(args)?;
if tx_id.is_some() && tx_slate_id.is_some() {
let msg = "At most one of 'id' (-i) or 'txid' (-t) may be provided.".to_string();
return Err(ParseError::ArgumentError(msg));
@@ -1055,8 +1058,7 @@ where
} else {
None
};
let mut owner_api = Owner::new(wallet.clone(), None, config.config_file_path.clone());
command::unpack(&mut owner_api, mask.as_ref(), slatepack)?;
command::unpack(wallet.clone(), mask.as_ref(), slatepack)?;
return Ok("unpack".to_string());
}
("owner_api", _) => {
@@ -1237,7 +1239,7 @@ where
("unpack", Some(args)) => {
let a = arg_parse!(parse_unpack_args(args));
let slatepack = command::read_slatepack(a)?;
command::unpack(owner_api, km, slatepack)
command::unpack(owner_api.wallet_inst.clone(), km, slatepack)
}
("finalize", Some(args)) => {
let a = arg_parse!(parse_finalize_args(args));