merge staging and resolve conflict

This commit is contained in:
wiesche
2026-10-15 21:20:50 +02:00
43 changed files with 2309 additions and 1003 deletions
Generated
+1
View File
@@ -2457,6 +2457,7 @@ dependencies = [
"serde_json",
"thiserror 1.0.69",
"url",
"uuid",
]
[[package]]
+1
View File
@@ -36,6 +36,7 @@ linefeed = "0.6"
semver = "1.0.28"
rustyline = "9"
lazy_static = "1"
uuid = { version = "1.23.4", features = ["v4"] }
grin_wallet_api = { path = "./api", version = "5.5.1" }
grin_wallet_impls = { path = "./impls", version = "5.5.1" }
+170 -54
View File
@@ -14,6 +14,7 @@
//! Owner API External Definition
use crate::config::config::{reload_global_config, update_global_config, WALLET_CONFIG_FILE_NAME};
use crate::config::{TorConfig, WalletConfig};
use crate::core::core::OutputFeatures;
use crate::core::global;
@@ -23,24 +24,25 @@ use crate::keychain::{Identifier, Keychain};
use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage};
use crate::libwallet::api_impl::types::update_tx_slate_state;
use crate::libwallet::api_impl::{owner, owner_updater};
use crate::libwallet::mwixnet::{parse_mwixnet_response, MwixnetResponse, SwapReq};
use crate::libwallet::mwixnet::{MixnetReqCreationParams, MwixnetReqCreationResult};
use crate::libwallet::slatepack::SlatepackAddressIndex;
use crate::libwallet::wallet_lock;
use crate::libwallet::RetrieveTxQueryArgs;
use crate::libwallet::{
AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient,
NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress,
TxLogEntry, ViewWallet, WalletInfo, WalletLCProvider,
TxLogEntry, ViewWallet, WalletBackend, WalletInfo, WalletLCProvider,
};
use crate::util::logger::LoggingConfig;
use crate::util::secp::{key::SecretKey, pedersen::Commitment};
use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString};
use grin_wallet_config::config::{
reload_global_config, update_global_config, WALLET_CONFIG_FILE_NAME,
};
use grin_wallet_libwallet::mwixnet::{MixnetReqCreationParams, SwapReq};
use grin_wallet_libwallet::RetrieveTxQueryArgs;
use grin_wallet_util::OnionV3Address;
use impls::json_rpc;
use impls::tor::arti::tor_post;
use chrono::prelude::*;
use ed25519_dalek::SigningKey as DalekSecretKey;
use libwallet::wallet_lock;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Write;
@@ -782,7 +784,7 @@ where
pub fn init_send_tx(
&self,
keychain_mask: Option<&SecretKey>,
args: InitTxArgs,
mut args: InitTxArgs,
) -> Result<Slate, Error> {
let send_args = args.send_args.clone();
let dest = match send_args.as_ref() {
@@ -797,6 +799,18 @@ where
.map(|_| crate::tor_config::load(&self.config_path()))
.transpose()?;
let init_send_tx = |w: &mut WalletBackend<C, K>, args: &mut InitTxArgs| {
if args.address_index.is_none() && args.estimate_only != Some(true) {
let parent_key_id = match &args.src_acct_name {
Some(name) => w.get_acct_path(name.clone())?.map(|account| account.path),
None => None,
}
.unwrap_or_else(|| w.parent_key_id());
args.address_index = Some(w.next_tx_slatepack_address_index(&parent_key_id)?);
}
owner::init_send_tx(w, keychain_mask, args.clone(), self.doctest_mode)
};
// Helper functionality. If send arguments exist, attempt to send sync and
// finalize
match send_args {
@@ -804,7 +818,7 @@ where
let slate = {
wallet_lock!(self.wallet_inst, w);
let late_lock = args.late_lock.unwrap_or(false);
let slate = owner::init_send_tx(w, keychain_mask, args, self.doctest_mode)?;
let slate = init_send_tx(w, &mut args)?;
let tc = tor_config.as_ref().ok_or_else(|| {
Error::TorConfig("Tor config was not loaded with send arguments".into())
})?;
@@ -815,12 +829,29 @@ where
if !late_lock {
owner::tx_lock_outputs(w, keychain_mask, &slate)?;
}
slate
};
let res =
try_slatepack_sync_workflow(&slate, &dest.unwrap(), tor_config, None, false);
let res = try_slatepack_sync_workflow(
&slate,
dest.as_ref().unwrap(),
tor_config,
None,
false,
);
match res {
Ok(s) => {
// Output slatepack message to file.
match output_slatepack_file(
self,
keychain_mask,
&s,
dest,
args.address_index,
) {
Ok(_) => {}
Err(e) => error!("Error on saving output slatepack message: {}", e),
}
let ret_slate = self.finalize_tx(keychain_mask, &s)?;
if sa.post_tx {
let result = self.post_tx(keychain_mask, &ret_slate, sa.fluff);
@@ -846,7 +877,7 @@ where
}
None => {
wallet_lock!(self.wallet_inst, w);
let slate = owner::init_send_tx(w, keychain_mask, args, self.doctest_mode)?;
let slate = init_send_tx(w, &mut args)?;
Ok(slate)
}
}
@@ -938,7 +969,7 @@ where
/// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml"));
///
/// // . . .
/// // The slate has been recieved from the invoicer, somehow
/// // The slate has been received from the invoicer, somehow
/// # let slate = Slate::blank(2, true);
/// let args = InitTxArgs {
/// src_acct_name: None,
@@ -970,19 +1001,17 @@ where
}
_ => None,
};
let (slate, tor_config) = {
let tor_config = send_args
.as_ref()
.map(|_| crate::tor_config::load(&self.config_path()))
.transpose()?;
let slate = {
let mut w_lock = self.wallet_inst.lock();
let tor_config = send_args
.as_ref()
.map(|_| crate::tor_config::load(&self.config_path()))
.transpose()?;
let w = w_lock.lc_provider()?.wallet_inst()?;
let slate =
owner::process_invoice_tx(w, keychain_mask, slate, args, self.doctest_mode)?;
(slate, tor_config)
owner::process_invoice_tx(w, keychain_mask, slate, args.clone(), self.doctest_mode)?
};
// Helper functionality. If send arguments exist, attempt to send
match send_args {
match send_args.as_ref() {
Some(sa) => {
let tc = tor_config.ok_or_else(|| {
Error::TorConfig("Tor config was not loaded with send arguments".into())
@@ -1006,7 +1035,13 @@ where
}
}
// Output slatepack message to file.
match output_slatepack_file(self, keychain_mask, &s, Some(dest)) {
match output_slatepack_file(
self,
keychain_mask,
&s,
Some(dest),
args.address_index,
) {
Ok(_) => {}
Err(e) => error!("Error on saving output slatepack message: {}", e),
}
@@ -1284,6 +1319,7 @@ where
&tx,
tx_id,
tx_slate_id,
true,
)
}
@@ -2193,7 +2229,7 @@ where
/// # Arguments
///
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if
/// * `derivation_index` - The index along the derivation path to retrieve an address for
/// * `derivation_index` - The index along the derivation path to retrieve an address for, should be not higher than 2^31 - 1
///
/// # Returns
/// * Ok with a SlatepackAddress representing the address
@@ -2205,13 +2241,14 @@ where
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// use grin_core::global::ChainTypes;
/// use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
///
/// use std::time::Duration;
///
/// // Set up as above
/// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml"));
///
/// let res = api_owner.get_slatepack_address(None, 0);
/// let res = api_owner.get_slatepack_address(None, SlatepackAddressIndex(0));
///
/// if let Ok(_) = res {
/// // ...
@@ -2221,7 +2258,7 @@ where
pub fn get_slatepack_address(
&self,
keychain_mask: Option<&SecretKey>,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<SlatepackAddress, Error> {
owner::get_slatepack_address(self.wallet_inst.clone(), keychain_mask, derivation_index)
}
@@ -2231,9 +2268,8 @@ where
///
/// # Arguments
///
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if
/// * `derivation_index` - The index along the derivation path to for which to retrieve the secret key
///
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using
/// * `derivation_index` - The index along the derivation path to for which to retrieve the secret key, should be not higher than 2^31 - 1
/// # Returns
/// * Ok with an ed25519_dalek::SecretKey if successful
/// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered.
@@ -2244,13 +2280,14 @@ where
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// use grin_core::global::ChainTypes;
/// use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
///
/// use std::time::Duration;
///
/// // Set up as above
/// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml"));
///
/// let res = api_owner.get_slatepack_secret_key(None, 0);
/// let res = api_owner.get_slatepack_secret_key(None, SlatepackAddressIndex(0));
///
/// if let Ok(_) = res {
/// // ...
@@ -2260,7 +2297,7 @@ where
pub fn get_slatepack_secret_key(
&self,
keychain_mask: Option<&SecretKey>,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<DalekSecretKey, Error> {
owner::get_slatepack_secret_key(self.wallet_inst.clone(), keychain_mask, derivation_index)
}
@@ -2271,7 +2308,7 @@ where
/// # Arguments
///
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if
/// * `sender_index` - If Some(n), the index along the derivation path to include as the sender
/// * `sender_index` - If Some(n), the index along the derivation path to include as the sender, should be not higher than 2^31 - 1
/// * `recipients` - Optional recipients for which to encrypt the slatepack's payload (i.e. the
/// slate). If an empty vec, the payload will remain unencrypted
///
@@ -2285,6 +2322,7 @@ where
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// use grin_core::global::ChainTypes;
/// use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
///
/// use std::time::Duration;
///
@@ -2310,7 +2348,7 @@ where
/// let slatepack = api_owner.create_slatepack_message(
/// None,
/// &slate,
/// Some(0),
/// Some(SlatepackAddressIndex(0)),
/// vec![],
/// );
/// }
@@ -2320,7 +2358,7 @@ where
&self,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
sender_index: Option<u32>,
sender_index: Option<SlatepackAddressIndex>,
recipients: Vec<SlatepackAddress>,
) -> Result<String, Error> {
owner::create_slatepack_message(
@@ -2340,8 +2378,7 @@ where
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if
/// * `slatepack` - A string representing an armored slatepack
/// * `secret_indices` - Indices along this wallet's derivation path with which to attempt
/// decryption. This function will attempt to use secret keys at each index along this path
/// to attempt to decrypt the payload, returning an error if none of the keys match.
/// decryption. This function will attempt to use secret keys at each index along this path to attempt to decrypt the payload, returning an error if none of the keys match. Should be not higher than 2^31 - 1 each.
///
/// # Returns
/// * Ok with a [Slate](../grin_wallet_libwallet/slate/struct.Slate.html) if successful
@@ -2353,6 +2390,7 @@ where
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// use grin_core::global::ChainTypes;
/// use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
///
/// use std::time::Duration;
///
@@ -2363,14 +2401,14 @@ where
/// let res = api_owner.slate_from_slatepack_message(
/// None,
/// slatepack_string,
/// vec![0, 1, 2],
/// vec![SlatepackAddressIndex(0), SlatepackAddressIndex(1), SlatepackAddressIndex(2)],
/// );
/// ```
pub fn slate_from_slatepack_message(
&self,
keychain_mask: Option<&SecretKey>,
slatepack: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slate, Error> {
owner::slate_from_slatepack_message(
self.wallet_inst.clone(),
@@ -2389,8 +2427,7 @@ where
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using
/// * `slatepack` - A string representing an armored slatepack
/// * `secret_indices` - Indices along this wallet's derivation path with which to attempt
/// decryption. If this wallet can't decrypt this slatepack, the payload of the returned
/// Slatepack will remain encrypted.
/// decryption. If this wallet can't decrypt this slatepack, the payload of the returned Slatepack will remain encrypted. Should be not higher than 2^31 - 1 each.
///
/// # Returns
/// * Ok with a [Slatepack](../grin_wallet_libwallet/slatepack/types/struct.Slatepack.html) if successful
@@ -2402,6 +2439,7 @@ where
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// use grin_core::global::ChainTypes;
/// use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
///
/// use std::time::Duration;
///
@@ -2412,7 +2450,7 @@ where
/// let res = api_owner.decode_slatepack_message(
/// None,
/// slatepack_string,
/// vec![0, 1, 2],
/// vec![SlatepackAddressIndex(0), SlatepackAddressIndex(1), SlatepackAddressIndex(2)],
/// );
///
/// ```
@@ -2420,7 +2458,7 @@ where
&self,
keychain_mask: Option<&SecretKey>,
slatepack: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slatepack, Error> {
owner::decode_slatepack_message(
self.wallet_inst.clone(),
@@ -2570,21 +2608,22 @@ where
// MWIXNET
/// Creates a mwixnet request [SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html)
/// Creates a mwixnet request [SwapReq](grin_wallet_libwallet::mwixnet::SwapReq)
/// from a given output commitment under this wallet's control.
///
/// # Arguments
/// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if
/// being used.
/// * `params` - A [MixnetReqCreationParams](../grin_wallet_libwallet/api_impl/types/struct.MixnetReqCreationParams.html)
/// * `params` - A [MixnetReqCreationParams]
/// struct containing the parameters for the request, which include:
/// `server_keys` - The public keys of the servers participating in the mixnet (each encoded internally as a `SecretKey`)
/// `fee_per_hop` - The fee to be paid to each server for each hop in the mixnet
/// `server_keys` - Published X25519 onion keys in route order, not Ed25519 identity keys
/// or private keys
/// `fee_per_hop` - Optional fee per hop with the swap minimum as default
/// * `commitment` - The commitment of the output to be mixed
/// * `lock_output` - Whether to lock the referenced output after creating the request
///
/// # Returns
/// * Ok([SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html)) if successful
/// * Ok([MwixnetReqCreationResult](../grin_wallet_libwallet/mwixnet/struct.MwixnetReqCreationResult.html)) if successful
/// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered
///
/// # Example
@@ -2595,8 +2634,10 @@ where
/// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml"));
/// let keychain_mask = None;
/// let params = MixnetReqCreationParams {
/// server_keys: vec![], // Public keys here in secret key representation
/// fee_per_hop: 100,
/// server_keys: vec![libwallet::mwixnet::MwixnetServerPublicKey::from_hex(
/// "24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a",
/// ).unwrap()],
/// fee_per_hop: None,
/// };
///
/// let commitment = Commitment::from_vec(vec![0; 32]);
@@ -2609,7 +2650,7 @@ where
/// lock_output,
/// );
///
/// if let Ok(req) = result {
/// if let Ok(result) = result {
/// //...
/// }
/// ```
@@ -2619,9 +2660,8 @@ where
params: &MixnetReqCreationParams,
commitment: &Commitment,
lock_output: bool, // use_test_rng: bool,
) -> Result<SwapReq, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
) -> Result<MwixnetReqCreationResult, Error> {
wallet_lock!(self.wallet_inst, w);
owner::create_mwixnet_req(
w,
keychain_mask,
@@ -2631,6 +2671,81 @@ where
self.doctest_mode,
)
}
/// Sends a mwixnet request [SwapReq](grin_wallet_libwallet::mwixnet::SwapReq)
/// to provided onion server.
///
/// # Arguments
/// * `req` - A [SwapReq]
/// struct containing the parameters for the request, which include:
/// `comsig` - [ComSignature](grin_wallet_libwallet::mwixnet::ComSignature) signature with a pedersen commitment value & blinding factors as the keys
/// `onion` - [Onion](grin_wallet_libwallet::mwixnet::Onion) encrypted route payload
/// * `server` - Onion server address
///
/// # Returns
/// * `Ok(None)` if the server accepted the request
/// * `Ok(Some(message))` if the server rejected the request
/// * `Err` for configuration, transport, or invalid-response failures
///
/// Leaves output locks unchanged
///
/// # Example
/// Set up as in [`new`](struct.Owner.html#method.new) method above.
/// ```
/// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config);
///
/// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml"));
/// let keychain_mask = None;
/// let params = MixnetReqCreationParams {
/// server_keys: vec![libwallet::mwixnet::MwixnetServerPublicKey::from_hex(
/// "24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a",
/// ).unwrap()],
/// fee_per_hop: None,
/// };
///
/// let commitment = Commitment::from_vec(vec![0; 32]);
/// let lock_output = true;
///
/// let result = api_owner.create_mwixnet_req(
/// keychain_mask,
/// &params,
/// &commitment,
/// lock_output,
/// );
///
/// if let Ok(result) = result {
/// let server = "mza3u6vkqodqc6kjjapfbcjrg7hgpkuw6nyq5xvfjqp5jm2lfeaocwqd.onion".try_into().unwrap();
/// let result = api_owner.send_mwixnet_req(&result.request, server);
/// if let Ok(result) = result {
/// //..
/// }
/// }
/// ```
pub fn send_mwixnet_req(
&self,
req: &SwapReq,
server: OnionV3Address,
) -> Result<Option<String>, Error> {
let url = format!("{}/v1", server.to_http_str());
let rpc_params = serde_json::json!([req]);
let rpc_request = json_rpc::build_request("swap", &rpc_params);
let tor_config = crate::tor_config::load(&self.config_path())?;
let response = match tor_post(&tor_config, &rpc_request, &url) {
Ok(response) => response,
Err(e) => {
return Err(Error::GenericError(format!(
"Could not determine whether mwixnet accepted the request: {}",
e,
)));
}
};
match parse_mwixnet_response(&response).map_err(|e| {
Error::GenericError(format!("Could not confirm the mwixnet response: {}", e))
})? {
MwixnetResponse::Accepted => Ok(None),
MwixnetResponse::Rejected(message) => Ok(Some(message)),
}
}
}
/// attempt to send slate synchronously with TOR
@@ -2758,6 +2873,7 @@ fn output_slatepack_file<L, C, K>(
keychain_mask: Option<&SecretKey>,
slate: &Slate,
dest: Option<SlatepackAddress>,
sender_index: Option<SlatepackAddressIndex>,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
@@ -2769,7 +2885,7 @@ where
Some(a) => vec![a],
None => vec![],
};
let message = api.create_slatepack_message(keychain_mask, slate, Some(0), recipients)?;
let message = api.create_slatepack_message(keychain_mask, slate, sender_index, recipients)?;
let tld = api.get_top_level_directory()?;
// Create a directory to which files will be output.
+43 -36
View File
@@ -14,7 +14,7 @@
//! JSON-RPC Stub generation for the Owner API
use grin_wallet_libwallet::RetrieveTxQueryArgs;
use libwallet::mwixnet::SwapReq;
use libwallet::mwixnet::MwixnetReqCreationResult;
use uuid::Uuid;
use crate::config::{TorConfig, WalletConfig};
@@ -22,10 +22,11 @@ use crate::core::core::OutputFeatures;
use crate::core::global;
use crate::keychain::{Identifier, Keychain};
use crate::libwallet::{
mwixnet::MixnetReqCreationParams, AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs,
IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate,
SlateVersion, Slatepack, SlatepackAddress, StatusMessage, TxLogEntry, VersionedSlate,
ViewWallet, WalletInfo, WalletLCProvider,
mwixnet::{MixnetReqCreationParams, MwixnetServerPublicKey},
AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient,
NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack,
SlatepackAddress, StatusMessage, TxLogEntry, VersionedSlate, ViewWallet, WalletInfo,
WalletLCProvider,
};
use crate::util::logger::LoggingConfig;
use crate::util::secp::key::{PublicKey, SecretKey};
@@ -33,6 +34,7 @@ use crate::util::secp::pedersen::Commitment;
use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString};
use crate::{ECDHPubkey, Ed25519SecretKey, Owner, Token};
use grin_wallet_util::OnionV3Address;
use libwallet::slatepack::SlatepackAddressIndex;
use rand::thread_rng;
use std::convert::TryFrom;
use std::path::PathBuf;
@@ -359,6 +361,7 @@ pub trait OwnerRpc {
true,
[
{
"address_index": null,
"amount_credited": "60000000000",
"amount_debited": "0",
"confirmation_ts": "2019-01-15T16:01:26Z",
@@ -380,6 +383,7 @@ pub trait OwnerRpc {
"tx_type": "ConfirmedCoinbase"
},
{
"address_index": null,
"amount_credited": "60000000000",
"amount_debited": "0",
"confirmation_ts": "2019-01-15T16:01:26Z",
@@ -451,6 +455,7 @@ pub trait OwnerRpc {
true,
[
{
"address_index": null,
"amount_credited": "60000000000",
"amount_debited": "0",
"confirmation_ts": "2019-01-15T16:01:26Z",
@@ -472,6 +477,7 @@ pub trait OwnerRpc {
"tx_type": "ConfirmedCoinbase"
},
{
"address_index": null,
"amount_credited": "60000000000",
"amount_debited": "0",
"confirmation_ts": "2019-01-15T16:01:26Z",
@@ -598,7 +604,7 @@ pub trait OwnerRpc {
"id": "0436430c-2b02-624c-2032-570501212b00",
"proof": {
"raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3",
"saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3"
"saddr": "4b2adcc4efc1d547c08ed7c9f7d987ff085f1788d62805e73c8f632e4b065039"
},
"sigs": [
{
@@ -1706,7 +1712,7 @@ pub trait OwnerRpc {
fn get_slatepack_address(
&self,
token: Token,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<SlatepackAddress, Error>;
/**
@@ -1740,7 +1746,7 @@ pub trait OwnerRpc {
fn get_slatepack_secret_key(
&self,
token: Token,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<Ed25519SecretKey, Error>;
/**
@@ -1790,7 +1796,7 @@ pub trait OwnerRpc {
&self,
token: Token,
slate: VersionedSlate,
sender_index: Option<u32>,
sender_index: Option<SlatepackAddressIndex>,
recipients: Vec<SlatepackAddress>,
) -> Result<String, Error>;
@@ -1844,7 +1850,7 @@ pub trait OwnerRpc {
&self,
token: Token,
message: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<VersionedSlate, Error>;
/**
@@ -1885,7 +1891,7 @@ pub trait OwnerRpc {
&self,
token: Token,
message: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slatepack, Error>;
/**
@@ -1917,6 +1923,7 @@ pub trait OwnerRpc {
"recipient_address": "tgrin10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqq6t83r",
"recipient_sig": "02868f2d2b983981f8f98043701687a8531ed2de564ea3df48e9e7e0229ccbe8359efe506896df2efbe3528e977252c50e4a41ca3cc9896e7c5a30bbb1d33604",
"sender_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp",
"sender_address_path": 0,
"sender_sig": "c511764f3f61ed3d1cbca9514df8bc6811fad5662b1cb0e0587b9c9e49db9f33183cce71af6cb24b507fabf525a2bc405c6e84e63a60334edff0b451ae5e6102"
}
}
@@ -1949,6 +1956,7 @@ pub trait OwnerRpc {
"recipient_address": "tgrin10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqq6t83r",
"recipient_sig": "02868f2d2b983981f8f98043701687a8531ed2de564ea3df48e9e7e0229ccbe8359efe506896df2efbe3528e977252c50e4a41ca3cc9896e7c5a30bbb1d33604",
"sender_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp",
"sender_address_path": 0,
"sender_sig": "c511764f3f61ed3d1cbca9514df8bc6811fad5662b1cb0e0587b9c9e49db9f33183cce71af6cb24b507fabf525a2bc405c6e84e63a60334edff0b451ae5e6102"
}
},
@@ -2055,7 +2063,11 @@ pub trait OwnerRpc {
) -> Result<BuiltOutput, Error>;
/**
Networked version of [Owner::build_output](struct.Owner.html#method.create_mwixnet_req).
Networked version of [Owner::create_mwixnet_req](struct.Owner.html#method.create_mwixnet_req).
server_keys accepts public X25519 onion keys as hex, in route order, replacing secret keys
fee_per_hop accepts a nanogrin string or null for the swap minimum
```
# grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!(
# r#"
@@ -2065,12 +2077,12 @@ pub trait OwnerRpc {
"params": {
"token": "d202964900000000d302964900000000d402964900000000d502964900000000",
"commitment": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7",
"fee_per_hop": "5000000",
"fee_per_hop": null,
"lock_output": true,
"server_keys": [
"97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332",
"0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e",
"b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5"
"24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a",
"3ab3b7d1fb643393897a0b942934d34be9cb823621cd49c09b12fdfa69598d71",
"80f2ceadcc5a767961772f4ae04826c70b0ae497f4f50d782241ff056b71df45"
]
},
"id": 1
@@ -2092,12 +2104,13 @@ pub trait OwnerRpc {
"62ea6b8369686a0415e1e752b9b4d6e66cf5b6066a2d3c60d8818890a55f3adff4601466f4c6e6b646568b99ae93549a3595b7a7b4be815ced87d9297cabbd69518d7b2ed6edd14007528fd346aaea765a1165fe886666627ebcab9588b8ee1c9e98395ae67913c48eb6e924581b40182fce807f97312fb07fd5e216d99941f2b488babce4078a50cd66b28b30a66c4f54fcc127437408a99b30ffd6c3d0d8c7d39e864fc04e321b8c10138c8852d4cad0a4f2780412b9dadcc6e0f2657b7803a81bccb809ca392464be2e01755be7377d0e815698ad6ea51d4617cc92c3ccf852f038e33cc9c90992438ba5c49cca7cc188b682da684e2f4c9733a84a7b64ac5c2216ebf5926f0ee67b664fb5bab799109cbee755ce1aebc8cd352fea51cd84c333cb958093c53544c3f3ab05dba64d8f041c3b179796b476ec04b11044e39db6994ab767315e52cc0ef023432ec88ade2911612db7e74e0923889f765b58b00e3869c5072a4e882c1b721913f63bda986b8c97b7ae575f0d4be596a1ac3cd0db96ce6074ee000b32018b3bda16d7dba34a13ba9c3ce983946414c16e278351a3411cb8ef2cb8ef5b6e1667c4c58bc797c0324ae4fec8960d684e561c0e833ee4c3331c6c439b59042a62993535e23cc8a8a4cf705c0f9b1d62db4e3d76c22c01138800414b143ddff471e4df4413e842a1b41f43cc9647e47145fd6c86d4d1a34fb2f62f5a55b31c9353ee34743c548eff955f2d2143c1a86cbcb452104f96d0142db31153021bbeed995c71a92de8fb1f97269533a508085c543fcb3ee57000bb265e74187b858403aa97b6c7b085e5d5b6025cbfe5f6926d33c835f90e60fc62013e80bbe0a855da5938b4b8f83ac29c5e8251827795356222079a6d1612e2fdf93bd7836d1613c7a353ada48ce256f880bbbb3108e037e3b5647101bd4d549101b0ee73d2248a932a802a3b1beb0b69d777c4285d57e91d83e96fe2f8a1a2f182fe2c6ca37b18460cf8d7f56c201147b9be19f1d01f8ad305c1e9c4dd79b5d8719d6550432352cf737082b1e9de7a083ffbe1"
],
"pubkey": "e7ee7d51b11d09f268ade98bc9d7ae9be3c4ac124ce1c3a40e50d34460fa5f08"
}
},
"tx_id": 5
}
}
}
# "#
# , 5, true, true, false, false);
# , 5, true, false, false, false);
```
*
*/
@@ -2105,10 +2118,10 @@ pub trait OwnerRpc {
&self,
token: Token,
commitment: String,
fee_per_hop: String,
fee_per_hop: Option<String>,
lock_output: bool,
server_keys: Vec<String>,
) -> Result<SwapReq, Error>;
) -> Result<MwixnetReqCreationResult, Error>;
}
impl<L, C, K> OwnerRpc for Owner<L, C, K>
@@ -2424,7 +2437,7 @@ where
fn get_slatepack_address(
&self,
token: Token,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<SlatepackAddress, Error> {
Owner::get_slatepack_address(self, token.keychain_mask.as_ref(), derivation_index)
}
@@ -2432,7 +2445,7 @@ where
fn get_slatepack_secret_key(
&self,
token: Token,
derivation_index: u32,
derivation_index: SlatepackAddressIndex,
) -> Result<Ed25519SecretKey, Error> {
let key =
Owner::get_slatepack_secret_key(self, token.keychain_mask.as_ref(), derivation_index)?;
@@ -2443,7 +2456,7 @@ where
&self,
token: Token,
slate: VersionedSlate,
sender_index: Option<u32>,
sender_index: Option<SlatepackAddressIndex>,
recipients: Vec<SlatepackAddress>,
) -> Result<String, Error> {
let res = Owner::create_slatepack_message(
@@ -2460,7 +2473,7 @@ where
&self,
token: Token,
message: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<VersionedSlate, Error> {
let slate = Owner::slate_from_slatepack_message(
self,
@@ -2476,7 +2489,7 @@ where
&self,
token: Token,
message: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slatepack, Error> {
Owner::decode_slatepack_message(self, token.keychain_mask.as_ref(), message, secret_indices)
}
@@ -2523,28 +2536,22 @@ where
&self,
token: Token,
commitment: String,
fee_per_hop: String,
fee_per_hop: Option<String>,
lock_output: bool,
server_keys: Vec<String>,
) -> Result<SwapReq, Error> {
) -> Result<MwixnetReqCreationResult, Error> {
let commit = Commitment::from_vec(from_hex(&commitment).map_err(Error::CommitDeser)?);
let secp_inst = static_secp_instance();
let secp = secp_inst.lock();
let mut keys = vec![];
for key in server_keys {
keys.push(SecretKey::from_slice(
&secp,
&grin_util::from_hex(&key).map_err(Error::ServerKeyDeser)?,
)?)
keys.push(MwixnetServerPublicKey::from_hex(&key).map_err(Error::ServerKeyDeser)?)
}
let req_params = MixnetReqCreationParams {
server_keys: keys,
fee_per_hop: fee_per_hop
.parse::<u64>()
.map_err(|_| Error::U64Deser(fee_per_hop))?,
.map(|fee| fee.parse::<u64>().map_err(|_| Error::U64Deser(fee)))
.transpose()?,
};
Owner::create_mwixnet_req(
+192 -69
View File
@@ -19,19 +19,24 @@ use crate::apiwallet::{try_slatepack_sync_workflow, Owner};
use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME};
use crate::core::{core, global};
use crate::error::Error;
use crate::impls::PathToSlatepack;
use crate::impls::SlateGetter as _;
use crate::keychain;
use crate::libwallet::api_impl::owner::{self, decrypt_slatepack, deserialize_slatepack};
use crate::libwallet::api_impl::types::update_tx_slate_state;
use crate::libwallet::mwixnet::MixnetReqCreationParams;
use crate::libwallet::slatepack::get_slatepack_file_contents;
use crate::libwallet::slatepack::SlatepackAddressIndex;
use crate::libwallet::OutputCommitMapping;
use crate::libwallet::{
self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack,
SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider,
SlatepackAddress, WalletLCProvider,
};
use crate::libwallet::{wallet_lock, WalletHandle};
use crate::util::secp::key::SecretKey;
use crate::util::{Mutex, ZeroingString};
use crate::util::secp::pedersen::Commitment;
use crate::util::{Mutex, ToHex, ZeroingString};
use crate::{controller, display};
use grin_wallet_util::OnionV3Address;
use grin_wallet_libwallet::wallet_lock;
use qr_code::QrCode;
use serde_json as json;
use std::fs::File;
@@ -341,6 +346,7 @@ pub struct SendArgs {
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
pub address_index: Option<SlatepackAddressIndex>,
}
fn max_retry_args(mut init_args: InitTxArgs, amount: u64, max_inputs: u32) -> InitTxArgs {
@@ -412,6 +418,7 @@ where
selection_strategy_is_use_all: strategy == "all",
refresh_outputs_from_node: !info_updated,
estimate_only: Some(true),
address_index: args.address_index,
..Default::default()
};
let result = owner_api.init_send_tx(keychain_mask, init_args.clone());
@@ -456,6 +463,7 @@ where
payment_proof_recipient_address,
ttl_blocks: args.ttl_blocks,
late_lock: Some(args.late_lock),
address_index: args.address_index,
..Default::default()
};
let init_send_tx = |init_args: InitTxArgs| -> Result<Slate, libwallet::Error> {
@@ -505,6 +513,11 @@ where
}
let output_sp = |owner_api: &mut Owner<L, C, K>, lock_outputs: bool| -> Result<(), Error> {
let sender_index = {
wallet_lock!(owner_api.wallet_inst, w);
let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?;
context.payment_proof_derivation_index
};
Ok(output_slatepack(
owner_api,
keychain_mask,
@@ -514,6 +527,7 @@ where
lock_outputs,
false,
args.slatepack_qr,
sender_index,
)?)
};
@@ -567,6 +581,7 @@ pub fn output_slatepack<L, C, K>(
lock: bool,
finalizing: bool,
show_qr: bool,
sender_index: Option<SlatepackAddressIndex>,
) -> Result<(), libwallet::Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
@@ -579,7 +594,8 @@ where
Some(a) => vec![a],
None => vec![],
};
let message = owner_api.create_slatepack_message(keychain_mask, slate, Some(0), recipients)?;
let message =
owner_api.create_slatepack_message(keychain_mask, slate, sender_index, recipients)?;
let tld = owner_api.get_top_level_directory()?;
// create a directory to which files will be output
@@ -653,42 +669,30 @@ where
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
let mut ret_address = None;
let slate = match filename {
let (slate, sender_address) = match filename {
Some(f) => {
// otherwise, get slate from slatepack
let dec_key = owner_api.get_slatepack_secret_key(keychain_mask, 0)?;
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: Some(&dec_key),
});
let pts = PathToSlatepack::new(f.into(), &packer, true);
let sl = Some(pts.get_tx()?.0);
ret_address = pts.get_slatepack(true)?.sender;
sl
let data = get_slatepack_file_contents(&f.into())?;
let sp = deserialize_slatepack(
&data,
owner_api.wallet_inst.clone(),
keychain_mask,
vec![SlatepackAddressIndex(0)],
)?;
let sl = sp.get_slate()?;
(sl, sp.sender)
}
None => None,
};
let slate = match slate {
Some(s) => s,
None => {
// try and parse directly from input_slatepack_message
match message {
Some(message) => {
let slate = owner_api.slate_from_slatepack_message(
let sp = owner_api.decode_slatepack_message(
keychain_mask,
message.clone(),
vec![0],
vec![SlatepackAddressIndex(0)],
)?;
let slatepack = owner_api.decode_slatepack_message(
keychain_mask,
message.clone(),
vec![0],
)?;
ret_address = slatepack.sender;
slate
let sl = sp.get_slate()?;
(sl, sp.sender)
}
None => {
let msg = "No slate provided via file or direct input";
@@ -697,7 +701,7 @@ where
}
}
};
Ok((slate, ret_address))
Ok((slate, sender_address))
}
/// Receive command argument
@@ -775,6 +779,7 @@ where
false,
false,
args.slatepack_qr,
None,
)?)
};
@@ -790,8 +795,7 @@ where
Ok(s) => {
// Update slate state.
{
let mut w_lock = owner_api.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
wallet_lock!(owner_api.wallet_inst, w);
let parent_key_id = w.parent_key_id();
match update_tx_slate_state(w, keychain_mask, &parent_key_id, &s) {
Ok(_) => {}
@@ -814,25 +818,18 @@ where
}
pub fn read_slatepack(args: ReceiveArgs) -> Result<Slatepack, Error> {
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
});
let slatepack = match args.input_file {
Some(f) => PathToSlatepack::new(f.into(), &packer, true).get_slatepack(false)?,
let data = match args.input_file {
Some(f) => get_slatepack_file_contents(&f.into())?,
None => match args.input_slatepack_message {
Some(message) => packer.deser_slatepack(message.as_bytes(), false)?,
None => {
return Err(Error::ArgumentError("Invalid Slatepack Input".into()));
}
Some(message) => message.into_bytes(),
None => return Err(Error::ArgumentError("Invalid Slatepack Input".into())),
},
};
Ok(slatepack)
Ok(owner::parse_slatepack(&data)?)
}
pub fn unpack<L, C, K>(
owner_api: &mut Owner<L, C, K>,
wallet_inst: WalletHandle<'static, L, C, K>,
keychain_mask: Option<&SecretKey>,
mut slatepack: Slatepack,
) -> Result<(), Error>
@@ -847,33 +844,32 @@ where
println!("{}", slatepack);
println!("------------------");
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
});
if slatepack.mode == 1 {
let dec_key = owner_api.get_slatepack_secret_key(keychain_mask, 0)?;
match slatepack.try_decrypt_payload(Some(&dec_key)) {
Ok(_) => {
match decrypt_slatepack(
&mut slatepack,
wallet_inst,
keychain_mask,
vec![SlatepackAddressIndex(0)],
)
.and_then(|_| slatepack.get_slate())
{
Ok(s) => {
println!("Slatepack is encrypted for this wallet");
println!();
println!("DECRYPTED SLATEPACK");
println!("-------------------");
println!("{}", slatepack);
let slate = packer.get_slate(&slatepack)?;
println!();
println!("DECRYPTED SLATE");
println!("---------------");
println!("{}", slate);
println!("{}", s);
}
Err(_) => {
println!("Slatepack payload cannot be decrypted by this wallet");
}
}
} else {
let slate = packer.get_slate(&slatepack)?;
let slate = slatepack.get_slate()?;
println!("Slatepack is not encrypted");
println!();
println!("SLATE");
@@ -904,7 +900,7 @@ where
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
let (mut slate, _ret_address) = parse_slatepack(
let (mut slate, _) = parse_slatepack(
owner_api,
keychain_mask,
args.input_file.clone(),
@@ -975,6 +971,7 @@ where
false,
true,
args.slatepack_qr,
None,
)?;
Ok(())
@@ -984,11 +981,11 @@ where
pub struct IssueInvoiceArgs {
/// Slatepack address
pub dest: Option<String>,
/// issue invoice tx args
/// Issue invoice tx args
pub issue_args: IssueInvoiceTxArgs,
/// output file override
/// Output file override
pub outfile: Option<String>,
/// show slatepack as QR code
/// Show slatepack as QR code
pub slatepack_qr: bool,
}
@@ -1009,8 +1006,13 @@ where
.transpose()?;
let issue_args = args.issue_args.clone();
let slate = owner_api.issue_invoice_tx(keychain_mask, issue_args)?;
let slate = owner_api.issue_invoice_tx(keychain_mask, issue_args.clone())?;
let sender_index = {
wallet_lock!(owner_api.wallet_inst, w);
let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?;
context.payment_proof_derivation_index
};
output_slatepack(
owner_api,
keychain_mask,
@@ -1020,6 +1022,7 @@ where
false,
false,
args.slatepack_qr,
sender_index,
)?;
Ok(())
}
@@ -1133,6 +1136,7 @@ where
true,
false,
args.slatepack_qr,
None,
)?)
};
@@ -1314,7 +1318,7 @@ where
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
let (slate, _ret_address) = parse_slatepack(
let (slate, _) = parse_slatepack(
owner_api,
keychain_mask,
args.input_file,
@@ -1462,7 +1466,7 @@ where
}
}
/// Payment Proof Address
/// Show wallet's slatepack address.
pub fn address<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
@@ -1472,8 +1476,10 @@ where
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
// Just address at derivation index 0 for now
let address = owner_api.get_slatepack_address(keychain_mask, 0)?;
// Just address at derivation index 0 for now,
// cause sender encrypts message for address,
// and we don't know its derivation path index.
let address = owner_api.get_slatepack_address(keychain_mask, SlatepackAddressIndex(0))?;
let account = account_label(owner_api)?;
println!();
println!("Address for account - {}", account);
@@ -1596,6 +1602,123 @@ where
}
}
pub struct MwixnetArgs {
pub server: OnionV3Address,
pub output: MwixnetOutput,
pub minimum_confirmations: u64,
pub params: MixnetReqCreationParams,
}
pub enum MwixnetOutput {
Commitment(Commitment),
MinimumAmount(u64),
Max,
}
fn confirm_mwixnet_response(
response: Result<Option<String>, libwallet::Error>,
tx_id: u32,
) -> Result<(), Error> {
let message = match response {
Ok(None) => {
println!("MWixnet request accepted (transaction {})", tx_id);
return Ok(());
}
Ok(Some(message)) => format!("MWixnet rejected the request: {}", message),
Err(error) => error.to_string(),
};
// The server may already have accepted the request
Err(Error::GenericError(format!(
"{}. Output remains locked as transaction {}. The server may already have accepted \
the request. To cancel manually, use `cancel -i {}`.",
message, tx_id, tx_id
)))
}
pub fn mwixnet<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
args: MwixnetArgs,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
let height = owner_api.node_height(keychain_mask)?.height;
let (refreshed, outputs) = owner_api.retrieve_outputs(keychain_mask, false, true, None)?;
if !refreshed {
return Err(Error::GenericError(
"Could not refresh wallet outputs. MWixnet request was not created".to_string(),
));
}
let eligible = |output: &&OutputCommitMapping| {
output
.output
.eligible_to_spend(height, args.minimum_confirmations.max(1))
};
let output = match &args.output {
MwixnetOutput::Commitment(commitment) => outputs
.iter()
.find(|output| output.commit == *commitment && eligible(output)),
MwixnetOutput::MinimumAmount(amount) => outputs
.iter()
.filter(eligible)
.filter(|output| output.output.value >= *amount)
.min_by_key(|output| output.output.value),
MwixnetOutput::Max => outputs
.iter()
.filter(eligible)
.max_by_key(|output| output.output.value),
}
.ok_or_else(|| Error::GenericError("No eligible MWixnet output was found".to_string()))?;
let commitment = output.commit;
println!(
"Selected MWixnet output {} with value {}",
commitment.to_hex(),
core::amount_to_hr_string(output.output.value, false)
);
let creation = owner_api.create_mwixnet_req(keychain_mask, &args.params, &commitment, true)?;
match creation.tx_id {
None => Err(Error::GenericError(
"MWixnet request was created without locking its output".to_string(),
)),
Some(tx_id) => {
let res = owner_api.send_mwixnet_req(&creation.request, args.server);
confirm_mwixnet_response(res, tx_id)
}
}
}
#[cfg(test)]
mod tests {
use super::confirm_mwixnet_response;
#[test]
fn mwixnet_response_reports_uncertain_transactions_as_locked() {
assert!(confirm_mwixnet_response(Ok(None), 3).is_ok());
let error =
confirm_mwixnet_response(Ok(Some("invalid response".to_string())), 3).unwrap_err();
assert!(error
.to_string()
.contains("remains locked as transaction 3"));
assert!(error.to_string().contains("cancel -i 3"));
let error = confirm_mwixnet_response(
Err(super::libwallet::Error::GenericError(
"transport failed".into(),
)),
4,
)
.unwrap_err()
.to_string();
assert!(error.contains("transport failed"));
assert!(error.contains("remains locked as transaction 4"));
assert!(error.contains("cancel -i 4"));
assert!(error.contains("may already have accepted"));
}
}
#[cfg(test)]
mod send_tests {
use super::*;
+4 -2
View File
@@ -55,6 +55,7 @@ use grin_wallet_config::config::{
};
use grin_wallet_impls::tor::arti::{start_tor_service, stop_tor_service};
use grin_wallet_impls::tor::process::TorProcess;
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use tokio::sync::mpsc;
@@ -255,8 +256,9 @@ where
let w_inst = lc.wallet_inst()?;
let k = w_inst.keychain(mask.as_ref())?;
let parent_key_id = w_inst.parent_key_id();
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let sec_key =
address::address_from_derivation_path(&k, &parent_key_id, SlatepackAddressIndex(0))
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
+3 -2
View File
@@ -20,7 +20,7 @@ use crate::libwallet::{
TxLogEntry, TxLogEntryType, ViewWallet, WalletInfo,
};
use crate::util::ToHex;
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
use prettytable;
use prettytable::format::{FormatBuilder, LinePosition, LineSeparator};
use std::io::prelude::Write;
@@ -541,7 +541,8 @@ pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
bMG->"Spendable balance",
]);
for m in acct_mappings {
let slatepack_path = address::address_derivation_path(&m.path, 0).to_bip_32_string();
let slatepack_path =
address::address_derivation_path(&m.path, SlatepackAddressIndex(0)).to_bip_32_string();
let spendable = if let Some(info) = m.info {
amount_to_hr_string(info.amount_currently_spendable, true)
} else {
+2 -1
View File
@@ -32,6 +32,7 @@ use grin_wallet_libwallet::{InitTxArgs, IssueInvoiceTxArgs};
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
/// self send impl
fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), libwallet::Error> {
@@ -235,7 +236,7 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(),
format!("{}/standard_pp_S3.txbin", test_dir),
),
};
let address = Some(api2.get_slatepack_address(mask2, 0)?);
let address = Some(api2.get_slatepack_address(mask2, SlatepackAddressIndex(0))?);
// send to send
let args = InitTxArgs {
+158 -9
View File
@@ -18,13 +18,17 @@ extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
use grin_core as core;
use grin_core::core::FeeFields;
use grin_util as util;
use grin_util::secp::key::SecretKey;
use std::path::PathBuf;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self, LocalWalletClient};
use libwallet::{mwixnet::MixnetReqCreationParams, InitTxArgs};
use libwallet::{
mwixnet::{MixnetReqCreationParams, MwixnetServerPublicKey, MAX_MWIXNET_HOPS},
InitTxArgs, OutputStatus, TxLogEntryType,
};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
@@ -141,22 +145,167 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
let server_key_3 =
SecretKey::from_slice(&secp, &grin_util::from_hex(server_pubkey_str_3).unwrap())?;
let params = MixnetReqCreationParams {
server_keys: vec![server_key_1, server_key_2, server_key_3],
fee_per_hop: 50_000_000,
server_keys: vec![
MwixnetServerPublicKey::from_secret(&server_key_1),
MwixnetServerPublicKey::from_secret(&server_key_2),
MwixnetServerPublicKey::from_secret(&server_key_3),
],
fee_per_hop: None,
};
let outputs = api1.retrieve_outputs(mask1, false, false, None)?;
// get last output
let last_output = outputs.1[outputs.1.len() - 1].clone();
let mwixnet_req = api1.create_mwixnet_req(mask1, &params, &last_output.commit, true)?;
// Store the output with an MMR index, like a restored output
let mmr_index = Some(chain.get_output_pos(&last_output.commit).unwrap());
{
wallet_inst!(wallet1, w);
let mut batch = w.batch(mask1)?;
let mut output = last_output.output.clone();
batch.delete(&output.key_id, &output.mmr_index)?;
output.mmr_index = mmr_index;
batch.save(output)?;
batch.commit()?;
}
println!("MWIXNET REQ: {:?}", mwixnet_req);
let empty_params = MixnetReqCreationParams {
server_keys: vec![],
fee_per_hop: params.fee_per_hop,
};
assert!(api1
.create_mwixnet_req(mask1, &empty_params, &last_output.commit, true)
.is_err());
// check output we created comsig for is indeed locked
let too_many_params = MixnetReqCreationParams {
server_keys: vec![params.server_keys[0]; MAX_MWIXNET_HOPS + 1],
fee_per_hop: params.fee_per_hop,
};
assert!(api1
.create_mwixnet_req(mask1, &too_many_params, &last_output.commit, true)
.is_err());
let oversized_fee_params = MixnetReqCreationParams {
server_keys: params.server_keys[..2].to_vec(),
fee_per_hop: Some(1 << 39),
};
assert_eq!(
api1.create_mwixnet_req(mask1, &oversized_fee_params, &last_output.commit, true)
.unwrap_err(),
libwallet::Error::Fee("mwixnet total fee exceeds FeeFields limit".to_string())
);
for fee in [0, 12_499_999] {
let invalid_params = MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: Some(fee),
};
assert_eq!(
api1.create_mwixnet_req(mask1, &invalid_params, &last_output.commit, true)
.unwrap_err(),
libwallet::Error::Fee("mwixnet fee is below the swap minimum 0.0125".to_string())
);
}
let custom_params = MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: Some(25_000_000),
};
let custom = api1.create_mwixnet_req(mask1, &custom_params, &last_output.commit, false)?;
let mut onion = custom.request.onion;
for server_key in [&server_key_1, &server_key_2, &server_key_3] {
let peeled = onion.peel_layer(server_key).unwrap();
assert_eq!(peeled.payload.fee, FeeFields::try_from(25_000_000u64)?);
onion = peeled.onion;
}
drop(secp);
// Failed refresh must not create or lock a swap
let tx_count = api1.retrieve_txs(mask1, false, None, None, None)?.1.len();
let (disconnected, receiver) = std::sync::mpsc::channel();
drop(receiver);
let original_proxy = std::mem::replace(&mut *client1.proxy_tx.lock(), disconnected);
let mut api1 = api1;
let result = wallet::command::mwixnet(
&mut api1,
mask1,
wallet::command::MwixnetArgs {
server: "mza3u6vkqodqc6kjjapfbcjrg7hgpkuw6nyq5xvfjqp5jm2lfeaocwqd.onion".try_into()?,
output: wallet::command::MwixnetOutput::Commitment(last_output.commit),
minimum_confirmations: 1,
params: MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: None,
},
},
);
*client1.proxy_tx.lock() = original_proxy;
assert!(result
.unwrap_err()
.to_string()
.contains("Could not refresh wallet outputs"));
assert_eq!(
api1.retrieve_txs(mask1, false, None, None, None)?.1.len(),
tx_count
);
let outputs = api1.retrieve_outputs(mask1, false, false, None)?.1;
assert_eq!(
outputs
.iter()
.find(|o| o.commit == last_output.commit)
.unwrap()
.output
.status,
OutputStatus::Unspent
);
let creation = api1.create_mwixnet_req(mask1, &params, &last_output.commit, true)?;
let creation_tx_id = creation.tx_id.unwrap();
let fee_per_hop = 12_500_000;
let mut onion = creation.request.onion.clone();
for server_key in [&server_key_1, &server_key_2, &server_key_3] {
let peeled = onion
.peel_layer(server_key)
.map_err(|e| libwallet::Error::GenericError(e.to_string()))?;
assert_eq!(peeled.payload.fee, FeeFields::try_from(fee_per_hop)?);
onion = peeled.onion;
}
println!("MWIXNET REQ: {:?}", creation.request);
// Check the input lock and expected output are tracked together.
let outputs = api1.retrieve_outputs(mask1, false, false, None)?;
// get last output
let last_output = outputs.1[outputs.1.len() - 1].clone();
assert_eq!(last_output.output.status, libwallet::OutputStatus::Locked);
let input = outputs
.1
.iter()
.find(|o| o.commit == last_output.commit)
.unwrap();
assert_eq!(input.output.status, OutputStatus::Locked);
assert_eq!(input.output.mmr_index, mmr_index);
let total_fee = fee_per_hop * params.server_keys.len() as u64;
let expected_amount = last_output.output.value - total_fee;
let expected_output = outputs
.1
.iter()
.find(|o| o.output.status == OutputStatus::Unconfirmed && o.output.value == expected_amount)
.unwrap();
assert_eq!(
input.output.tx_log_entry,
expected_output.output.tx_log_entry
);
let txs = api1.retrieve_txs(mask1, false, None, None, None)?.1;
let tx = txs.last().unwrap();
assert_eq!(creation_tx_id, tx.id);
assert_eq!(tx.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx.amount_debited, last_output.output.value);
assert_eq!(tx.amount_credited, expected_amount);
assert_eq!(tx.num_inputs, 1);
assert_eq!(tx.num_outputs, 1);
assert_eq!(tx.fee, Some(FeeFields::try_from(total_fee)?));
assert!(api1
.create_mwixnet_req(mask1, &params, &last_output.commit, false)
.is_err());
// let logging finish
stopper.store(false, Ordering::Relaxed);
+3 -2
View File
@@ -28,6 +28,7 @@ use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
/// Various tests on accounts within the same wallet
fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
@@ -78,7 +79,7 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err
let _ =
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
let address = Some(api2.get_slatepack_address(mask2, 0)?);
let address = Some(api2.get_slatepack_address(mask2, SlatepackAddressIndex(0))?);
println!("Public address is: {:?}", address);
let amount = 60_000_000_000;
@@ -121,7 +122,7 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err
slate_i.payment_proof.as_ref().unwrap().receiver_address
);
assert!(pp.receiver_signature.is_some());
assert_eq!(pp.sender_address_path, 0);
assert_eq!(pp.sender_address_path, SlatepackAddressIndex(1));
assert_eq!(pp.sender_signature, None);
// check we should get an error at this point since proof is not complete
+4 -1
View File
@@ -30,6 +30,7 @@ use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
use grin_wallet_libwallet::slatepack::SlatepackAddressIndex;
/// self send impl
fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
@@ -171,7 +172,9 @@ fn late_lock_self_send_proof() -> Result<(), libwallet::Error> {
amount: core::consensus::REWARD,
minimum_confirmations: 2,
late_lock: Some(true),
payment_proof_recipient_address: Some(api1.get_slatepack_address(mask1, 0)?),
payment_proof_recipient_address: Some(
api1.get_slatepack_address(mask1, SlatepackAddressIndex(0))?,
),
..Default::default()
},
)?;
+298 -93
View File
@@ -20,25 +20,30 @@ extern crate grin_wallet_impls as impls;
use grin_core as core;
use grin_wallet_config::GlobalWalletConfig;
use grin_wallet_libwallet as libwallet;
use std::path::PathBuf;
use impls::test_framework::{self, LocalWalletClient};
use impls::{PathToSlatepack, SlatePutter as _};
use impls::DefaultLCProvider;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::{fs, thread};
use grin_wallet_libwallet::{
InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, Slate, Slatepack, SlatepackAddress,
Slatepacker, SlatepackerArgs,
slatepack, Error, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, Slate, Slatepack,
SlatepackAddress, SlatepackBin, WalletHandle,
};
use ed25519_dalek::SigningKey as edDalekSecretKey;
use ed25519_dalek::VerifyingKey as edDalekPublicKey;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
use grin_keychain::ExtKeychain;
use grin_util::secp::SecretKey;
use grin_wallet_libwallet::api_impl::owner::{decrypt_slatepack, deserialize_slatepack};
use grin_wallet_libwallet::slatepack::{get_slatepack_file_contents, SlatepackAddressIndex};
use grin_wallet_util::byte_ser;
fn output_slatepack(
slate: &Slate,
@@ -47,35 +52,55 @@ fn output_slatepack(
use_bin: bool,
sender: Option<SlatepackAddress>,
recipients: Vec<SlatepackAddress>,
) -> Result<(), libwallet::Error> {
let packer = Slatepacker::new(SlatepackerArgs {
sender,
recipients,
dec_key: None,
});
) -> Result<(), Error> {
let mut file = file.into();
if armored {
file = format!("{}.armored", file);
}
PathToSlatepack::new(file.into(), &packer, armored).put_tx(slate, use_bin)
let slatepack = slate.create_slatepack(sender, recipients)?;
let mut pub_tx = File::create(&file)?;
if use_bin {
if armored {
let armored = slatepack.armor()?;
pub_tx.write_all(armored.as_bytes())?;
} else {
pub_tx.write_all(
&byte_ser::to_bytes(&SlatepackBin(slatepack)).map_err(|_| Error::SlatepackSer)?,
)?;
}
} else {
pub_tx.write_all(
serde_json::to_string_pretty(&slatepack)
.map_err(|_| Error::SlateSer)?
.as_bytes(),
)?;
}
pub_tx.sync_all()?;
Ok(())
}
fn slate_from_packed(
wallet_inst: WalletHandle<
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
LocalWalletClient,
ExtKeychain,
>,
keychain_mask: Option<&SecretKey>,
file: &str,
armored: bool,
dec_key: Option<&edDalekSecretKey>,
) -> Result<(Slatepack, Slate), libwallet::Error> {
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key,
});
) -> Result<(Slatepack, Slate), Error> {
let mut file = file.into();
if armored {
file = format!("{}.armored", file);
}
let slatepack = PathToSlatepack::new(file.into(), &packer, armored).get_slatepack(true)?;
Ok((slatepack.clone(), packer.get_slate(&slatepack)?))
let data = get_slatepack_file_contents(&file.into())?;
let slatepack = deserialize_slatepack(
&data,
wallet_inst,
keychain_mask,
vec![SlatepackAddressIndex(0)],
)?;
Ok((slatepack.clone(), slatepack.get_slate()?))
}
/// self send impl
@@ -84,7 +109,7 @@ fn slatepack_exchange_test_impl(
use_bin: bool,
use_armored: bool,
use_encryption: bool,
) -> Result<(), libwallet::Error> {
) -> Result<(), Error> {
// Create a new proxy to simulate server and wallet responses
let mut wallet_proxy = create_wallet_proxy(test_dir);
let chain = wallet_proxy.chain.clone();
@@ -134,41 +159,49 @@ fn slatepack_exchange_test_impl(
api2.create_account_path(mask2, "account1")?;
api2.create_account_path(mask2, "account2")?;
// Get some mining done
{
wallet_inst!(wallet1, w);
w.set_account_by_name("mining")?;
}
{
wallet_inst!(wallet2, w);
w.set_account_by_name("account1")?;
}
// Get some mining done
let mut bh = 10u64;
let _ =
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
let (recipients_1, dec_key_1, sender_1) = match use_encryption {
let (recipients_1, sender_1) = match use_encryption {
true => {
let sec_key = api1.get_slatepack_secret_key(mask1, 0)?;
let sec_key = api1.get_slatepack_secret_key(mask1, SlatepackAddressIndex(0))?;
let pub_key = edDalekPublicKey::from(&sec_key);
let rec_address = SlatepackAddress::new(&pub_key);
(
vec![rec_address.clone()],
Some(sec_key),
Some(rec_address.clone()),
)
let sen_index = SlatepackAddressIndex(7);
let sec_key = api1.get_slatepack_secret_key(mask1, sen_index)?;
let pub_key = edDalekPublicKey::from(&sec_key);
let sen_address = SlatepackAddress::new(&pub_key);
(vec![rec_address.clone()], Some(sen_address.clone()))
}
false => (vec![], None, None),
false => (vec![], None),
};
let (recipients_2, dec_key_2, sender_2) = match use_encryption {
let (recipients_2, sender_2) = match use_encryption {
true => {
let sec_key = api2.get_slatepack_secret_key(mask2, 0)?;
let sec_key = api2.get_slatepack_secret_key(mask2, SlatepackAddressIndex(0))?;
let pub_key = edDalekPublicKey::from(&sec_key);
let rec_address = SlatepackAddress::new(&pub_key);
(
vec![rec_address.clone()],
Some(sec_key),
Some(rec_address.clone()),
)
let sen_index = SlatepackAddressIndex(11);
let sec_key = api2.get_slatepack_secret_key(mask2, sen_index)?;
let pub_key = edDalekPublicKey::from(&sec_key);
let sen_address = SlatepackAddress::new(&pub_key);
(vec![rec_address.clone()], Some(sen_address.clone()))
}
false => (vec![], None, None),
false => (vec![], None),
};
let (send_file, receive_file, final_file) = match use_bin {
@@ -190,7 +223,7 @@ fn slatepack_exchange_test_impl(
assert_eq!(wallet1_info.total, bh * reward);
// send to send
let args = InitTxArgs {
src_acct_name: Some("mining".to_owned()),
address_index: Some(SlatepackAddressIndex(7)),
amount: reward * 2,
minimum_confirmations: 2,
max_outputs: 500,
@@ -210,14 +243,8 @@ fn slatepack_exchange_test_impl(
)?;
api1.tx_lock_outputs(mask1, &slate)?;
// Get some mining done
{
wallet_inst!(wallet2, w);
w.set_account_by_name("account1")?;
}
let (mut slatepack, mut slate) =
slate_from_packed(&send_file, use_armored, dec_key_2.as_ref())?;
let (slatepack, mut slate) =
slate_from_packed(wallet2.clone(), mask2, &send_file, use_armored)?;
// wallet 2 receives file, completes, sends file back
wallet::controller::foreign_single_use(
@@ -231,8 +258,8 @@ fn slatepack_exchange_test_impl(
&receive_file,
use_armored,
use_bin,
// re-encrypt for sender!
sender_2.clone(),
// re-encrypt for sender!
match slatepack.sender.clone() {
Some(s) => vec![s.clone()],
None => vec![],
@@ -243,23 +270,34 @@ fn slatepack_exchange_test_impl(
)?;
// wallet 1 finalizes and posts
let (_, mut slate) = slate_from_packed(&receive_file, use_armored, dec_key_1.as_ref())?;
let (slatepack, mut slate) =
slate_from_packed(wallet1.clone(), mask1, &receive_file, use_armored)?;
slate = api1.finalize_tx(mask1, &slate)?;
// Output final file for reference
output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?;
output_slatepack(
&slate,
&final_file,
use_armored,
use_bin,
sender_1.clone(),
match slatepack.sender {
Some(s) => vec![s.clone()],
None => vec![],
},
)?;
api1.post_tx(mask1, &slate, false)?;
bh += 1;
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
bh += 3;
// Check total in mining account
// Check total in wallet 1 account
let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, bh * reward - reward * 2);
// Check total in 'wallet 2' account
// Check total in wallet 2 account
let (wallet2_refreshed, wallet2_info) = api2.retrieve_summary_info(mask2, true, 1)?;
assert!(wallet2_refreshed);
assert_eq!(wallet2_info.last_confirmed_height, bh);
@@ -281,10 +319,11 @@ fn slatepack_exchange_test_impl(
};
let args = IssueInvoiceTxArgs {
address_index: Some(SlatepackAddressIndex(11)),
amount: 1000000000,
..Default::default()
};
let mut slate = api2.issue_invoice_tx(mask2, args)?;
let slate = api2.issue_invoice_tx(mask2, args)?;
output_slatepack(
&slate,
&send_file,
@@ -303,9 +342,8 @@ fn slatepack_exchange_test_impl(
selection_strategy_is_use_all: true,
..Default::default()
};
let res = slate_from_packed(&send_file, use_armored, dec_key_1.as_ref())?;
slatepack = res.0;
slate = res.1;
let (slatepack, mut slate) =
slate_from_packed(wallet1.clone(), mask1, &send_file, use_armored)?;
slate = api1.process_invoice_tx(mask1, &slate, args)?;
api1.tx_lock_outputs(mask1, &slate)?;
output_slatepack(
@@ -314,25 +352,16 @@ fn slatepack_exchange_test_impl(
use_armored,
use_bin,
sender_1.clone(),
match slatepack.sender.clone() {
match slatepack.sender {
Some(s) => vec![s.clone()],
None => vec![],
},
)?;
wallet::controller::foreign_single_use(
wallet2.clone(),
PathBuf::from(test_dir),
mask2_i.clone(),
|api| {
// Wallet 2 receives the invoice transaction
let res = slate_from_packed(&receive_file, use_armored, dec_key_2.as_ref())?;
slate = res.1;
slate = api.finalize_tx(&slate, false)?;
output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?;
Ok(())
},
)?;
api1.post_tx(mask1, &slate, false)?;
// Wallet 2 receives the invoice transaction
let (_, mut slate) = slate_from_packed(wallet2.clone(), mask2, &receive_file, use_armored)?;
slate = api2.finalize_tx(mask2, &slate)?;
output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?;
api2.post_tx(mask2, &slate, false)?;
// Standard, with payment proof
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
@@ -349,17 +378,17 @@ fn slatepack_exchange_test_impl(
),
};
let address = Some(api2.get_slatepack_address(mask2, 0)?);
// send to send
// send from 1st wallet
let args = InitTxArgs {
src_acct_name: Some("mining".to_owned()),
address_index: Some(SlatepackAddressIndex(7)),
amount: reward,
minimum_confirmations: 2,
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: true,
payment_proof_recipient_address: address.clone(),
payment_proof_recipient_address: Some(
api2.get_slatepack_address(mask2, SlatepackAddressIndex(0))?,
),
..Default::default()
};
let mut slate = api1.init_send_tx(mask1, args)?;
@@ -373,21 +402,21 @@ fn slatepack_exchange_test_impl(
)?;
api1.tx_lock_outputs(mask1, &slate)?;
// receive at 2nd wallet
wallet::controller::foreign_single_use(
wallet2.clone(),
PathBuf::from(test_dir),
mask2_i.clone(),
|api| {
let res = slate_from_packed(&send_file, use_armored, dec_key_2.as_ref())?;
let slatepack = res.0;
slate = res.1;
let (slatepack, mut slate) =
slate_from_packed(wallet2.clone(), mask2, &send_file, use_armored)?;
slate = api.receive_tx(&slate, None, None)?;
output_slatepack(
&slate,
&receive_file,
use_armored,
use_bin,
sender_2,
sender_2.clone(),
match slatepack.sender {
Some(s) => vec![s.clone()],
None => vec![],
@@ -398,7 +427,7 @@ fn slatepack_exchange_test_impl(
)?;
// wallet 1 finalizes and posts
let res = slate_from_packed(&receive_file, use_armored, dec_key_1.as_ref())?;
let res = slate_from_packed(wallet1.clone(), mask1, &receive_file, use_armored)?;
slate = res.1;
slate = api1.finalize_tx(mask1, &slate)?;
// Output final file for reference
@@ -450,7 +479,6 @@ fn slatepack_api_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
let args = InitTxArgs {
src_acct_name: Some("mining".to_owned()),
amount: reward * 2,
minimum_confirmations: 2,
max_outputs: 500,
@@ -458,15 +486,141 @@ fn slatepack_api_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
selection_strategy_is_use_all: true,
..Default::default()
};
for index in [SlatepackAddressIndex::MAX + 1, u32::MAX] {
assert!(matches!(
api1.init_send_tx(
mask1,
InitTxArgs {
address_index: Some(SlatepackAddressIndex(index)),
..args.clone()
}
),
Err(Error::SlatepackAddress(_))
));
assert!(matches!(
api1.issue_invoice_tx(
mask1,
IssueInvoiceTxArgs {
amount: reward,
address_index: Some(SlatepackAddressIndex(index)),
..Default::default()
}
),
Err(Error::SlatepackAddress(_))
));
}
api1.init_send_tx(
mask1,
InitTxArgs {
estimate_only: Some(true),
..args.clone()
},
)?;
let slate = api1.init_send_tx(mask1, args)?;
// create an encrypted slatepack (just encrypted for self)
let enc_addr = api1.get_slatepack_address(mask1, 0)?;
let slatepack = api1.create_slatepack_message(mask1, &slate, Some(0), vec![enc_addr])?;
println!("{}", slatepack);
let slatepack_raw = api1.decode_slatepack_message(mask1, slatepack.clone(), vec![0])?;
println!("{}", slatepack_raw);
let decoded_slate = api1.slate_from_slatepack_message(mask1, slatepack, vec![0])?;
println!("{}", decoded_slate);
let address_index = SlatepackAddressIndex(0);
let enc_addr = api1.get_slatepack_address(mask1, address_index)?;
let slatepack =
api1.create_slatepack_message(mask1, &slate, Some(address_index), vec![enc_addr])?;
let mut encrypted = api1.decode_slatepack_message(mask1, slatepack.clone(), vec![])?;
assert_eq!(encrypted.mode, 1);
decrypt_slatepack(&mut encrypted, wallet1.clone(), mask1, vec![address_index])?;
assert_eq!(encrypted.get_slate()?.id, slate.id);
let slatepack_raw =
api1.decode_slatepack_message(mask1, slatepack.clone(), vec![address_index])?;
assert_eq!(slatepack_raw.mode, 0);
assert_eq!(slatepack_raw.get_slate()?.id, slate.id);
let decoded_slate = api1.slate_from_slatepack_message(mask1, slatepack, vec![address_index])?;
assert_eq!(decoded_slate.id, slate.id);
let unknown_index = SlatepackAddressIndex(77);
let unknown_address = api1.get_slatepack_address(mask1, unknown_index)?;
let message = api1.create_slatepack_message(mask1, &slate, None, vec![unknown_address])?;
let encrypted = api1.decode_slatepack_message(mask1, message.clone(), vec![])?;
let decoded = api1.decode_slatepack_message(mask1, message.clone(), vec![address_index])?;
assert_eq!(decoded.mode, 1);
assert_eq!(decoded.payload, encrypted.payload);
assert!(matches!(
api1.slate_from_slatepack_message(mask1, message.clone(), vec![address_index]),
Err(Error::SlatepackDecryption(_))
));
let mut api1 = api1;
assert!(matches!(
wallet::command::parse_slatepack(&mut api1, mask1, None, Some(message)),
Err(wallet::Error::LibWallet(Error::SlatepackDecryption(_)))
));
let index = {
wallet_inst!(wallet1, w);
w.get_private_context(mask1, slate.id.as_bytes())?
.payment_proof_derivation_index
.unwrap()
};
assert_eq!(index, SlatepackAddressIndex(1));
let address = api1.get_slatepack_address(mask1, index)?;
let message = api1.create_slatepack_message(mask1, &slate, None, vec![address])?;
let decoded = api1.decode_slatepack_message(mask1, message.clone(), vec![address_index])?;
assert_eq!(decoded.mode, 0);
assert_eq!(decoded.get_slate()?.id, slate.id);
api1.create_account_path(mask1, "other")?;
api1.set_active_account(mask1, "other")?;
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 6, false)?;
api1.set_active_account(mask1, "default")?;
let invoice = api1.issue_invoice_tx(
mask1,
IssueInvoiceTxArgs {
amount: reward,
dest_acct_name: Some("other".into()),
..Default::default()
},
)?;
let send = api1.init_send_tx(
mask1,
InitTxArgs {
amount: reward,
src_acct_name: Some("other".into()),
minimum_confirmations: 2,
..Default::default()
},
)?;
api1.set_active_account(mask1, "other")?;
let next = api1.issue_invoice_tx(
mask1,
IssueInvoiceTxArgs {
amount: reward,
..Default::default()
},
)?;
for (slate, expected) in [(&invoice, 1), (&send, 2), (&next, 3)] {
wallet_inst!(wallet1, w);
let context = w.get_private_context(mask1, slate.id.as_bytes())?;
assert_eq!(context.parent_key_id, w.parent_key_id());
assert_eq!(
context.payment_proof_derivation_index,
Some(SlatepackAddressIndex(expected))
);
}
api1.set_active_account(mask1, "default")?;
let invoice = api1.issue_invoice_tx(
mask1,
IssueInvoiceTxArgs {
amount: reward,
..Default::default()
},
)?;
{
wallet_inst!(wallet1, w);
assert_eq!(
w.get_private_context(mask1, invoice.id.as_bytes())?
.payment_proof_derivation_index,
Some(SlatepackAddressIndex(2))
);
}
api1.close_wallet(None)?;
let encrypted = api1.decode_slatepack_message(None, message, vec![])?;
assert_eq!(encrypted.mode, 1);
// let logging finish
stopper.store(false, Ordering::Relaxed);
@@ -526,7 +680,6 @@ fn slatepack_address_validation(test_dir: &'static str) -> Result<(), libwallet:
)?;
let args = InitTxArgs {
src_acct_name: Some("mining".to_owned()),
amount: reward,
minimum_confirmations: 2,
max_outputs: 500,
@@ -566,6 +719,15 @@ fn slatepack_address_validation(test_dir: &'static str) -> Result<(), libwallet:
fluff: false,
skip_tor: Some(true),
});
let missing_config = PathBuf::from(test_dir).join("missing.toml");
let api = grin_wallet_api::Owner::new(wallet1.clone(), None, missing_config);
assert!(api
.process_invoice_tx(mask1, &slate, valid_args.clone())
.is_err());
{
wallet_inst!(wallet1, w);
assert!(w.get_private_context(mask1, slate.id.as_bytes()).is_err());
}
api1.process_invoice_tx(mask1, &slate, valid_args.clone())?;
api1.init_send_tx(mask1, valid_args)?;
@@ -660,3 +822,46 @@ fn slatepack_address() {
}
clean_output_dir(test_dir);
}
#[test]
fn slatepack_file_contents() {
let test_dir = "test_output/slatepack_file_contents";
setup(test_dir);
fs::create_dir_all(test_dir).unwrap();
let sp_path = PathBuf::from(test_dir).join("pack_file");
// set Slatepack file to minimum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::min_size()).unwrap();
}
let mut pack_path = get_slatepack_file_contents(&sp_path);
assert!(pack_path.is_ok());
// set Slatepack file to maximum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::max_size()).unwrap();
}
pack_path = get_slatepack_file_contents(&sp_path);
assert!(pack_path.is_ok());
// set Slatepack file below minimum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::min_size() - 1).unwrap();
}
pack_path = get_slatepack_file_contents(&sp_path);
assert!(pack_path.is_err());
// set Slatepack file above maximum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::max_size() + 1).unwrap();
}
pack_path = get_slatepack_file_contents(&sp_path);
assert!(pack_path.is_err());
clean_output_dir(test_dir);
}
+5 -44
View File
@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"jayson": "^4.3.0"
"jayson": "^5.0.0"
}
},
"node_modules/@types/connect": {
@@ -65,14 +65,6 @@
"es6-promise": "^4.0.3"
}
},
"node_modules/eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
"integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=",
"engines": {
"node": "> 0.1.90"
}
},
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
@@ -83,9 +75,9 @@
}
},
"node_modules/jayson": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
"integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-5.0.0.tgz",
"integrity": "sha512-FghxOWlJB5ZPsRsuMF1U4GFHjkJfOEYSlIHpI6Wt6MXIzvqWVo0Kpl7/SMfY36vWggA+b+L09zRGP6m4ROVnKg==",
"license": "MIT",
"dependencies": {
"@types/connect": "^3.4.33",
@@ -94,18 +86,15 @@
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"stream-json": "^1.9.1",
"uuid": "^8.3.2",
"ws": "^7.5.10"
},
"bin": {
"jayson": "bin/jayson.js"
},
"engines": {
"node": ">=8"
"node": ">=20"
}
},
"node_modules/json-stringify-safe": {
@@ -113,34 +102,6 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/uuid": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/ws": {
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
+1 -4
View File
@@ -9,9 +9,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"jayson": "^4.3.0"
},
"overrides": {
"uuid": "^11.1.1"
"jayson": "^5.0.0"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ The sample sends it as the password for the `grin` user. If `api_secret_path` is
Ensure the client connection settings in `src/index.js` match the Owner API address.
Then (assuming node.js and npm are installed on the system):
Then (assuming Node.js >=20 and npm are installed on the system):
```.sh
npm install
-2
View File
@@ -13,11 +13,9 @@
// limitations under the License.
mod file;
mod slatepack;
pub mod tor;
pub use self::file::PathToSlate;
pub use self::slatepack::PathToSlatepack;
pub use self::tor::TorSlateSender;
use crate::config::WalletConfig;
-179
View File
@@ -1,179 +0,0 @@
// Copyright 2021 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Slatepack Output 'plugin' implementation
use std::fs::{metadata, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::libwallet::{slatepack, Error, Slate, Slatepack, SlatepackBin, Slatepacker};
use crate::{SlateGetter, SlatePutter};
use grin_wallet_util::byte_ser;
// And Slate putter impls to output to files
pub struct PathToSlatepack<'a> {
pub pathbuf: PathBuf,
pub packer: &'a Slatepacker<'a>,
pub armor_output: bool,
}
impl<'a> PathToSlatepack<'a> {
/// Create with pathbuf and recipients
pub fn new(pathbuf: PathBuf, packer: &'a Slatepacker<'a>, armor_output: bool) -> Self {
Self {
pathbuf,
packer,
armor_output,
}
}
pub fn get_slatepack_file_contents(&self) -> Result<Vec<u8>, Error> {
let metadata = metadata(&self.pathbuf)?;
let len = metadata.len();
let min_len = slatepack::min_size();
let max_len = slatepack::max_size();
if len < min_len || len > max_len {
let msg = format!(
"Data is invalid length: {} | min: {}, max: {} |",
len, min_len, max_len
);
return Err(Error::SlatepackDeser(msg));
}
let mut pub_tx_f = File::open(&self.pathbuf)?;
let mut data = Vec::new();
pub_tx_f.read_to_end(&mut data)?;
Ok(data)
}
pub fn get_slatepack(&self, decrypt: bool) -> Result<Slatepack, Error> {
let data = self.get_slatepack_file_contents()?;
self.packer.deser_slatepack(&data, decrypt)
}
}
impl<'a> SlatePutter for PathToSlatepack<'a> {
fn put_tx(&self, slate: &Slate, as_bin: bool) -> Result<(), Error> {
let slatepack = self.packer.create_slatepack(slate)?;
let mut pub_tx = File::create(&self.pathbuf)?;
if as_bin {
if self.armor_output {
let armored = self.packer.armor_slatepack(&slatepack)?;
pub_tx.write_all(armored.as_bytes())?;
} else {
pub_tx.write_all(
&byte_ser::to_bytes(&SlatepackBin(slatepack))
.map_err(|_| Error::SlatepackSer)?,
)?;
}
} else {
pub_tx.write_all(
serde_json::to_string_pretty(&slatepack)
.map_err(|_| Error::SlateSer)?
.as_bytes(),
)?;
}
pub_tx.sync_all()?;
Ok(())
}
}
impl<'a> SlateGetter for PathToSlatepack<'a> {
fn get_tx(&self) -> Result<(Slate, bool), Error> {
let data = self.get_slatepack_file_contents()?;
let slatepack = self.packer.deser_slatepack(&data, true)?;
Ok((self.packer.get_slate(&slatepack)?, true))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use grin_core::global;
fn clean_output_dir(test_dir: &str) {
let _ = fs::remove_dir_all(test_dir);
}
fn setup(test_dir: &str) {
clean_output_dir(test_dir);
}
const SLATEPACK_DIR: &str = "target/test_output/slatepack";
#[test]
fn pathbuf_get_file_contents() {
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
setup(SLATEPACK_DIR);
fs::create_dir_all(SLATEPACK_DIR).unwrap();
let sp_path = PathBuf::from(SLATEPACK_DIR).join("pack_file");
// set Slatepack file to minimum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::min_size()).unwrap();
}
let args = slatepack::SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
};
let packer = Slatepacker::new(args);
let mut pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true);
assert!(pack_path.get_slatepack_file_contents().is_ok());
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false);
assert!(pack_path.get_slatepack_file_contents().is_ok());
// set Slatepack file to maximum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::max_size()).unwrap();
}
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true);
assert!(pack_path.get_slatepack_file_contents().is_ok());
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false);
assert!(pack_path.get_slatepack_file_contents().is_ok());
// set Slatepack file below minimum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::min_size() - 1).unwrap();
}
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true);
assert!(pack_path.get_slatepack_file_contents().is_err());
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false);
assert!(pack_path.get_slatepack_file_contents().is_err());
// set Slatepack file above maximum allowable size
{
let f = File::create(sp_path.clone()).unwrap();
f.set_len(slatepack::max_size() + 1).unwrap();
}
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true);
assert!(pack_path.get_slatepack_file_contents().is_err());
pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false);
assert!(pack_path.get_slatepack_file_contents().is_err());
}
}
+1 -1
View File
@@ -187,7 +187,7 @@ impl TorSlateSender {
client.send_request(req)?
} else {
tor_post(&self.config, &input, url)
.map_err(|e| ClientError::RequestError(format!("{:?}", e)))?
.map_err(|e| ClientError::RequestError(e.to_string()))?
};
Ok(res)
}
+2 -2
View File
@@ -42,9 +42,9 @@ pub mod test_framework;
pub mod tor;
pub use crate::adapters::{
PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, SlateSender,
TorSlateSender,
PathToSlate, SlateGetter, SlatePutter, SlateReceiver, SlateSender, TorSlateSender,
};
pub use crate::client_utils::json_rpc;
pub use crate::error::Error;
pub use crate::lifecycle::DefaultLCProvider;
pub use crate::node_clients::HTTPNodeClient;
+111 -34
View File
@@ -34,6 +34,7 @@ use sha2::Sha512;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
@@ -59,6 +60,18 @@ struct TorService {
state_lock: PathBuf,
}
/// Failure from an HTTP request over Tor, classified by whether delivery may
/// have started.
#[derive(Debug, thiserror::Error)]
pub enum TorPostError {
/// The failure happened before handing the request to Hyper.
#[error("{0}")]
NotSent(Error),
/// Hyper started sending the request, so delivery cannot be ruled out.
#[error("{0}")]
PossiblySent(Error),
}
type ArtiClientConfig = (Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig);
lazy_static! {
@@ -218,39 +231,73 @@ pub fn start_tor_client(config: TorConfig) -> Result<(), Error> {
Ok(())
}
/// Build an origin-form JSON POST request
fn build_post_request(json: String, url: &Uri) -> Result<Request<Full<Bytes>>, Error> {
let authority = url
.authority()
.ok_or_else(|| Error::GenericError(format!("URL {} has bad authority", url)))?;
let request_target = url
.path_and_query()
.map(|path| path.as_str())
.unwrap_or("/");
Request::builder()
.uri(request_target)
.method("POST")
.header("host", authority.as_str())
.header("accept", "application/json")
.header("content-type", "application/json")
.body(Full::from(json))
.map_err(|e| Error::TorProcess(format!("{:?}", e)))
}
/// Make POST request.
pub fn tor_post<IN>(tor_config: &TorConfig, input: &IN, url: &str) -> Result<String, Error>
pub fn tor_post<IN>(tor_config: &TorConfig, input: &IN, url: &str) -> Result<String, TorPostError>
where
IN: Serialize,
{
let json = serde_json::to_string(input)
.map_err(|_| Error::GenericError("Could not serialize data to JSON".to_owned()))?;
let json = serde_json::to_string(input).map_err(|_| {
TorPostError::NotSent(Error::GenericError(
"Could not serialize data to JSON".to_owned(),
))
})?;
let url = url.to_string();
let url: Uri = url
.parse()
.map_err(|_| Error::GenericError(format!("Bad URL: {}", url)))?;
.map_err(|_| TorPostError::NotSent(Error::GenericError(format!("Bad URL: {}", url))))?;
let host = match url.host() {
None => return Err(Error::GenericError(format!("URL {} has bad host", url))),
None => {
return Err(TorPostError::NotSent(Error::GenericError(format!(
"URL {} has bad host",
url
))))
}
Some(h) => h,
}
.to_string();
let port = url.port_u16().unwrap_or(80);
let request = build_post_request(json, &url).map_err(TorPostError::NotSent)?;
let timeout = tor_config.request_timeout();
let (state_path, cache_path) = state_cache_paths(tor_config);
let (client, _) = init_client(&state_path, &cache_path, tor_config)?;
let res: Result<String, Error> = thread::spawn(move || {
let (client, _) =
init_client(&state_path, &cache_path, tor_config).map_err(TorPostError::NotSent)?;
let res: Result<String, TorPostError> = thread::spawn(move || {
let c = client.clone();
client.runtime().block_on(async move {
let res = c
.runtime()
.timeout(timeout, async {
let stream = c
.connect((host, url.port_u16().unwrap_or(80)))
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let send_started = Arc::new(AtomicBool::new(false));
let send_started_in_request = send_started.clone();
let runtime = c.runtime().clone();
let res = runtime
.timeout(timeout, async move {
let stream = c.connect((host, port)).await.map_err(|e| {
TorPostError::NotSent(Error::TorProcess(format!("{:?}", e)))
})?;
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(TokioIo::new(stream))
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
.map_err(|e| {
TorPostError::NotSent(Error::TorProcess(format!("{:?}", e)))
})?;
// Spawn a task to poll the connection and drive the HTTP state.
tokio::spawn(async move {
@@ -259,36 +306,44 @@ where
}
});
let resp = request_sender
.send_request(
Request::builder()
.uri(url)
.method("POST")
.body::<Full<Bytes>>(Full::from(json))
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?,
)
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
send_started_in_request.store(true, Ordering::Release);
let resp = request_sender.send_request(request).await.map_err(|e| {
TorPostError::PossiblySent(Error::TorProcess(format!("{:?}", e)))
})?;
let body_resp = resp
.into_body()
.collect()
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let status = resp.status();
let body_resp = resp.into_body().collect().await.map_err(|e| {
TorPostError::PossiblySent(Error::TorProcess(format!("{:?}", e)))
})?;
let body = body_resp.to_bytes().into();
let body_text = String::from_utf8(body)
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let body_text = String::from_utf8(body).map_err(|e| {
TorPostError::PossiblySent(Error::TorProcess(format!("{:?}", e)))
})?;
if !status.is_success() {
return Err(TorPostError::PossiblySent(Error::TorProcess(format!(
"HTTP request failed with status {}: {}",
status, body_text
))));
}
Ok(body_text)
})
.await;
match res {
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
Err(e) if send_started.load(Ordering::Acquire) => Err(TorPostError::PossiblySent(
Error::TorProcess(format!("{:?}", e)),
)),
Err(e) => Err(TorPostError::NotSent(Error::TorProcess(format!("{:?}", e)))),
Ok(body) => Ok(body),
}
})
})
.join()
.unwrap_or_else(|e| Err(Error::TorProcess(format!("{:?}", e))))?;
.unwrap_or_else(|e| {
Err(TorPostError::PossiblySent(Error::TorProcess(format!(
"{:?}",
e
))))
})?;
res
}
@@ -300,6 +355,10 @@ fn init_client(
) -> Result<(Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig), Error> {
let mut builder = TorClientConfigBuilder::from_directories(state_path, cache_path);
builder.address_filter().allow_onion_addrs(true);
// Circuit timeout minimum applies to listeners and sends too
builder
.override_net_params()
.insert("cbtmintimeout".into(), 60_000);
// Configure bridge.
if let Some(bridge_line) = tor_config.bridge.bridge_line.as_ref() {
@@ -478,3 +537,21 @@ fn add_service_key(
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::build_post_request;
use hyper::{Method, Uri};
#[test]
fn builds_origin_form_json_request() {
let url: Uri = "http://example.onion:8080/v1?test=1".parse().unwrap();
let request = build_post_request("{}".to_string(), &url).unwrap();
assert_eq!(request.method(), Method::POST);
assert_eq!(request.uri(), "/v1?test=1");
assert_eq!(request.headers()["host"], "example.onion:8080");
assert_eq!(request.headers()["accept"], "application/json");
assert_eq!(request.headers()["content-type"], "application/json");
}
}
+13 -6
View File
@@ -15,12 +15,13 @@
//! Functions defining wallet 'addresses', i.e. ed2559 keys based on
//! a derivation path
use crate::blake2::blake2b::blake2b;
use crate::slatepack::SlatepackAddressIndex;
use crate::Error;
use grin_keychain::{ChildNumber, Identifier, Keychain, SwitchCommitmentType};
use grin_util::secp::key::SecretKey;
use crate::blake2::blake2b::blake2b;
/// Derive a Slatepack address path from an output parent path and index.
///
/// Output paths:
@@ -30,11 +31,14 @@ use crate::blake2::blake2b::blake2b;
/// Slatepack address paths:
/// - account m/0: m/0/1/0, m/0/1/1
/// - account m/1: m/1/1/0, m/1/1/1
pub fn address_derivation_path(parent_key_id: &Identifier, index: u32) -> Identifier {
pub fn address_derivation_path(
parent_key_id: &Identifier,
index: SlatepackAddressIndex,
) -> Identifier {
let mut key_path = parent_key_id.to_path();
key_path.path[1] = ChildNumber::from(1);
key_path.depth += 1;
key_path.path[key_path.depth as usize - 1] = ChildNumber::from(index);
key_path.path[key_path.depth as usize - 1] = ChildNumber::from(index.value());
Identifier::from_path(&key_path)
}
@@ -42,7 +46,7 @@ pub fn address_derivation_path(parent_key_id: &Identifier, index: u32) -> Identi
pub fn address_from_derivation_path<K>(
keychain: &K,
parent_key_id: &Identifier,
index: u32,
index: SlatepackAddressIndex,
) -> Result<SecretKey, Error>
where
K: Keychain,
@@ -62,6 +66,9 @@ mod tests {
fn slatepack_address_path() {
let parent = ExtKeychain::derive_key_id(2, 2, 0, 0, 0);
let expected = ExtKeychain::derive_key_id(3, 2, 1, 3, 0);
assert_eq!(address_derivation_path(&parent, 3), expected);
assert_eq!(
address_derivation_path(&parent, SlatepackAddressIndex(3)),
expected
);
}
}
+6 -1
View File
@@ -22,6 +22,7 @@ use crate::api_impl::types::update_tx_slate_state;
use crate::backend::WalletBackend;
use crate::internal::{selection, tx, updater};
use crate::slate_versions::SlateVersion;
use crate::slatepack::SlatepackAddressIndex;
use crate::{
address, BlockFees, CbData, Error, NodeClient, Slate, SlateState, TxLogEntryType, VersionInfo,
};
@@ -124,7 +125,11 @@ where
ret_slate.amount,
&excess,
p.sender_address,
address::address_from_derivation_path(&keychain, &parent_key_id, 0)?,
address::address_from_derivation_path(
&keychain,
&parent_key_id,
SlatepackAddressIndex(0),
)?,
)?;
p.receiver_signature = Some(sig);
+375 -174
View File
@@ -14,40 +14,42 @@
//! Generic implementation of owner API functions
use std::cmp;
use uuid::Uuid;
use crate::api_impl::foreign::finalize_tx as foreign_finalize;
use crate::util::OnionV3Address;
use grin_core::core::amount_to_hr_string;
use grin_core::core::hash::Hashed;
use grin_core::core::{FeeFields, Output, OutputFeatures, Transaction};
use grin_core::libtx::proof;
use grin_keychain::ViewKey;
use grin_util::secp::{key::SecretKey, pedersen::Commitment};
use grin_util::ToHex;
use crate::api_impl::owner_updater::StatusMessage;
use crate::internal::{keys, scan, selection, tx, updater};
use crate::slate::{PaymentInfo, Slate, SlateState};
use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletInfo};
use crate::slatepack::SlatepackAddressIndex;
use crate::types::{AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletInfo};
use crate::util::OnionV3Address;
use crate::{
address,
mwixnet::{create_onion, ComSignature, Hop, MixnetReqCreationParams, SwapReq},
wallet_lock, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult,
mwixnet::{
create_onion, ComSignature, Hop, MixnetReqCreationParams, MwixnetReqCreationResult,
SwapReq, MAX_MWIXNET_HOPS,
},
slatepack, wallet_lock, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult,
OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack,
SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletBackend,
SlatepackAddress, SlatepackArmor, SlatepackBin, TxLogEntryType, ViewWallet, WalletBackend,
WalletInitStatus, WalletLCProvider,
};
use grin_core::core::amount_to_hr_string;
use grin_core::core::hash::Hashed;
use grin_core::core::{FeeFields, Output, OutputFeatures, Transaction, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use grin_core::libtx::proof;
use grin_keychain::ViewKey;
use grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType};
use grin_util::secp::{key::SecretKey, pedersen::Commitment};
use grin_util::ToHex;
use grin_wallet_util::byte_ser;
use ed25519_dalek::SigningKey as DalekSecretKey;
use ed25519_dalek::Verifier;
use ed25519_dalek::VerifyingKey as DalekPublicKey;
use x25519_dalek::{PublicKey as xPublicKey, StaticSecret};
use std::cmp;
use std::convert::{TryFrom, TryInto};
use std::sync::mpsc::Sender;
use uuid::Uuid;
/// List of accounts
pub fn accounts<C, K>(
@@ -121,13 +123,14 @@ where
pub fn get_slatepack_address<'a, L, C, K>(
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
index: u32,
index: SlatepackAddressIndex,
) -> Result<SlatepackAddress, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
index.check()?;
wallet_lock!(wallet_inst, w);
let parent_key_id = w.parent_key_id();
let k = w.keychain(keychain_mask)?;
@@ -140,13 +143,14 @@ where
pub fn get_slatepack_secret_key<'a, L, C, K>(
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
index: u32,
index: SlatepackAddressIndex,
) -> Result<DalekSecretKey, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
index.check()?;
wallet_lock!(wallet_inst, w);
let parent_key_id = w.parent_key_id();
let k = w.keychain(keychain_mask)?;
@@ -160,7 +164,7 @@ pub fn create_slatepack_message<'a, L, C, K>(
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
sender_index: Option<u32>,
sender_index: Option<SlatepackAddressIndex>,
recipients: Vec<SlatepackAddress>,
) -> Result<String, Error>
where
@@ -168,17 +172,16 @@ where
C: NodeClient + 'a,
K: Keychain + 'a,
{
let sender = match sender_index {
Some(i) => Some(get_slatepack_address(wallet_inst, keychain_mask, i)?),
let sender = match sender_index.as_ref() {
Some(i) => Some(get_slatepack_address(
wallet_inst.clone(),
keychain_mask,
*i,
)?),
None => None,
};
let packer = Slatepacker::new(SlatepackerArgs {
sender,
recipients,
dec_key: None,
});
let slatepack = packer.create_slatepack(slate)?;
packer.armor_slatepack(&slatepack)
let slatepack = slate.create_slatepack(sender, recipients)?;
slatepack.armor()
}
/// Unpack a slate from the given slatepack message,
@@ -187,47 +190,16 @@ pub fn slate_from_slatepack_message<'a, L, C, K>(
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
slatepack: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slate, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
if secret_indices.is_empty() {
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
});
let slatepack = packer.deser_slatepack(slatepack.as_bytes(), true)?;
packer.get_slate(&slatepack)
} else {
for index in secret_indices {
let dec_key = Some(get_slatepack_secret_key(
wallet_inst.clone(),
keychain_mask,
index,
)?);
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: dec_key.as_ref(),
});
let res = packer.deser_slatepack(slatepack.as_bytes(), true);
let slatepack = match res {
Ok(sp) => sp,
Err(_) => {
continue;
}
};
return packer.get_slate(&slatepack);
}
Err(Error::SlatepackDecryption(
"Could not decrypt slatepack with any provided index on the address derivation path"
.to_owned(),
))
}
let sp = decode_slatepack_message(wallet_inst, keychain_mask, slatepack, secret_indices)
.map_err(|e| Error::SlatepackDecryption(format!("Could not decrypt slatepack: {}", e)))?;
sp.get_slate()
}
/// Decode a slatepack message, to allow viewing
@@ -237,43 +209,199 @@ pub fn decode_slatepack_message<'a, L, C, K>(
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
slatepack: String,
secret_indices: Vec<u32>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slatepack, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
});
if secret_indices.is_empty() {
packer.deser_slatepack(slatepack.as_bytes(), false)
} else {
for index in secret_indices {
let dec_key = Some(get_slatepack_secret_key(
wallet_inst.clone(),
keychain_mask,
index,
)?);
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: dec_key.as_ref(),
});
let res = packer.deser_slatepack(slatepack.as_bytes(), true);
let slatepack = match res {
Ok(sp) => sp,
Err(_) => {
continue;
}
};
return Ok(slatepack);
}
packer.deser_slatepack(slatepack.as_bytes(), false)
for i in &secret_indices {
i.check()?;
}
let slatepack = parse_slatepack(slatepack.as_bytes())?;
if slatepack.mode == 1 && !secret_indices.is_empty() {
let mut decrypted = slatepack.clone();
if decrypt_slatepack(&mut decrypted, wallet_inst, keychain_mask, secret_indices).is_ok() {
return Ok(decrypted);
}
}
Ok(slatepack)
}
/// Deserialize provided data to slatepack
pub fn deserialize_slatepack<'a, L, C, K>(
data: &[u8],
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<Slatepack, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut slatepack = parse_slatepack(data)?;
decrypt_slatepack(&mut slatepack, wallet_inst, keychain_mask, secret_indices)?;
Ok(slatepack)
}
/// Parse a slatepack without accessing wallet keys
pub fn parse_slatepack(data: &[u8]) -> Result<Slatepack, Error> {
// check if data is armored, if so, remove and continue
let data_len = data.len() as u64;
if data_len < slatepack::min_size() || data_len > slatepack::max_size() {
return Err(Error::SlatepackDeser("Data invalid length".to_string()));
}
let test_header = &data[..slatepack::HEADER.len()];
let data = match str::from_utf8(test_header) {
Ok(s) => {
if s == slatepack::HEADER {
SlatepackArmor::decode(data)?
} else {
data.to_vec()
}
}
Err(_) => data.to_vec(),
};
// try as bin first, then as json
let slatepack = match byte_ser::from_bytes::<SlatepackBin>(&data) {
Ok(s) => s.0,
Err(e) => {
debug!("Not a valid binary slatepack: {} - Will try JSON", e);
let content = String::from_utf8(data).map_err(|e| {
let msg = format!("{}", e);
Error::SlatepackDeser(msg)
})?;
serde_json::from_str(&content).map_err(|e| {
let msg = format!("Error reading JSON slatepack: {}", e);
Error::SlatepackDeser(msg)
})?
}
};
slatepack.ver_check_warn();
Ok(slatepack)
}
/// Decrypt a parsed slatepack using wallet indices
pub fn decrypt_slatepack<'a, L, C, K>(
slatepack: &mut Slatepack,
wallet_inst: crate::WalletHandle<'a, L, C, K>,
keychain_mask: Option<&SecretKey>,
mut secret_indices: Vec<SlatepackAddressIndex>,
) -> Result<(), Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
secret_indices.sort_unstable();
secret_indices.dedup();
if slatepack.mode == 1 {
// Try decrypt with selected indices.
let mut try_decrypt_with = |indices: &Vec<SlatepackAddressIndex>| -> Result<(), Error> {
for i in indices {
let dec_key = get_slatepack_secret_key(wallet_inst.clone(), keychain_mask, *i)?;
match slatepack.try_decrypt_payload(Some(&dec_key)) {
Ok(_) => return Ok(()),
Err(e) => {
if matches!(e, Error::Age(_)) {
continue;
} else {
return Err(e);
}
}
}
}
Err(Error::SlatepackDecryption(
"Can not decrypt slatepack with provided indexes".to_string(),
))
};
// Try decrypt with initial indices.
return match try_decrypt_with(&secret_indices) {
Ok(_) => Ok(()),
Err(_) => {
// Collect indexes for transactions.
let collect_indexes = |outstanding: bool,
secret_indices: &Vec<SlatepackAddressIndex>|
-> Result<Vec<SlatepackAddressIndex>, Error> {
wallet_lock!(wallet_inst, w);
let mut indices: Vec<SlatepackAddressIndex> = w
.tx_log_iter()?
.flatten()
.filter(|tx| {
let tx_type = if outstanding {
!tx.confirmed
&& (tx.tx_type == TxLogEntryType::TxReceived
|| tx.tx_type == TxLogEntryType::TxSent
|| tx.tx_type == TxLogEntryType::TxReverted)
} else {
tx.confirmed
&& tx.tx_type != TxLogEntryType::TxReceivedCancelled
&& tx.tx_type != TxLogEntryType::TxSentCancelled
};
tx_type && tx.parent_key_id == w.parent_key_id()
})
.filter_map(|t| t.address_index)
.filter(|i| !secret_indices.contains(i))
.collect();
indices.sort_unstable();
indices.dedup();
Ok(indices)
};
let mut indexes = collect_indexes(true, &secret_indices)?;
match try_decrypt_with(&mut indexes) {
Ok(_) => Ok(()),
Err(_) => {
for i in indexes {
if !secret_indices.contains(&i) {
secret_indices.push(i);
}
}
let indexes = collect_indexes(false, &secret_indices)?;
match try_decrypt_with(&indexes) {
Ok(()) => Ok(()),
Err(_) => {
for i in indexes {
if !secret_indices.contains(&i) {
secret_indices.push(i);
}
}
let mut indexes: Vec<SlatepackAddressIndex> = {
wallet_lock!(wallet_inst, w);
w.private_context_iter()?
.flatten()
.filter(|c| {
let index =
if let Some(i) = c.payment_proof_derivation_index {
!secret_indices.contains(&i)
} else {
false
};
c.parent_key_id == w.parent_key_id() && index
})
.map(|c| c.payment_proof_derivation_index.unwrap())
.collect()
};
indexes.sort_unstable();
indexes.dedup();
match try_decrypt_with(&indexes) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
}
}
}
}
};
}
Ok(())
}
/// retrieve outputs
@@ -532,6 +660,7 @@ where
recipient_sig: r_sig,
sender_address: SlatepackAddress::new(&proof.sender_address),
sender_sig: s_sig,
sender_address_path: Some(proof.sender_address_path),
})
}
@@ -546,6 +675,10 @@ where
C: NodeClient,
K: Keychain,
{
if let Some(index) = args.address_index {
index.check()?;
}
if args.amount == 0 {
return Err(Error::InvalidAmount);
}
@@ -629,15 +762,12 @@ where
)?
};
// Payment Proof, add addresses to slate and save address
// TODO: Note we only use single derivation path for now,
// probably want to allow sender to specify which one
let deriv_path = 0u32;
let address_index = args.address_index.unwrap_or(SlatepackAddressIndex(0));
if let Some(a) = payment_proof_address {
let k = w.keychain(keychain_mask)?;
let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, deriv_path)?;
let sec_addr_key =
address::address_from_derivation_path(&k, &parent_key_id, address_index)?;
let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?;
slate.payment_proof = Some(PaymentInfo {
@@ -645,14 +775,13 @@ where
receiver_address: a.pub_key,
receiver_signature: None,
});
context.payment_proof_derivation_index = Some(deriv_path);
}
// Save the aggsig context in our DB for when we
// recieve the transaction back
// receive the transaction back
{
let mut batch = w.batch(keychain_mask)?;
context.payment_proof_derivation_index = Some(address_index);
batch.save_private_context(slate.id.as_bytes(), &context)?;
batch.commit()?;
}
@@ -666,13 +795,17 @@ where
pub fn issue_invoice_tx<C, K>(
w: &mut WalletBackend<C, K>,
keychain_mask: Option<&SecretKey>,
args: IssueInvoiceTxArgs,
mut args: IssueInvoiceTxArgs,
use_test_rng: bool,
) -> Result<Slate, Error>
where
C: NodeClient,
K: Keychain,
{
if let Some(index) = args.address_index {
index.check()?;
}
if args.amount == 0 {
return Err(Error::InvalidAmount);
}
@@ -688,9 +821,13 @@ where
None => w.parent_key_id(),
};
if args.address_index.is_none() {
args.address_index = Some(w.next_tx_slatepack_address_index(&parent_key_id)?);
}
let mut slate = tx::new_tx_slate(w, args.amount, true, 2, use_test_rng, None)?;
let height = w.w2n_client().get_chain_tip()?.0;
let context = tx::add_output_to_slate(
let mut context = tx::add_output_to_slate(
w,
keychain_mask,
&mut slate,
@@ -704,10 +841,20 @@ where
slate.version_info.version = v;
};
// Update transaction log address index
let mut tx = w.get_tx_log_entry(&slate.id, &parent_key_id)?;
if let Some(tx) = tx.as_mut() {
tx.address_index = args.address_index;
let mut batch = w.batch(keychain_mask)?;
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
batch.commit()?;
}
// Save the aggsig context in our DB for when we
// recieve the transaction back
// receive the transaction back
{
let mut batch = w.batch(keychain_mask)?;
context.payment_proof_derivation_index = args.address_index;
batch.save_private_context(slate.id.as_bytes(), &context)?;
batch.commit()?;
}
@@ -813,6 +960,7 @@ where
// if self-sending, merge contexts
if let Ok(c) = context_res {
context.payment_proof_derivation_index = c.payment_proof_derivation_index;
context.initial_sec_key = c.initial_sec_key;
context.initial_sec_nonce = c.initial_sec_nonce;
context.fee = c.fee;
@@ -828,7 +976,7 @@ where
selection::repopulate_tx(w, keychain_mask, &mut ret_slate, &context, false)?;
// Save the aggsig context in our DB for when we
// recieve the transaction back
// receive the transaction back
{
let mut batch = w.batch(keychain_mask)?;
batch.save_private_context(slate.id.as_bytes(), &context)?;
@@ -895,18 +1043,20 @@ pub fn cancel_tx<'a, L, C, K>(
status_send_channel: &Option<Sender<StatusMessage>>,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
refresh_from_node: bool,
) -> Result<(), Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
if !update_wallet_state(
wallet_inst.clone(),
keychain_mask,
status_send_channel,
false,
)? {
if refresh_from_node
&& !update_wallet_state(
wallet_inst.clone(),
keychain_mask,
status_send_channel,
false,
)? {
return Err(Error::TransactionCancellationError(
"Can't contact running Grin node. Not Cancelling.",
));
@@ -1324,12 +1474,23 @@ where
};
// for now, simple test whether one of the addresses belongs to this wallet
let sec_key = address::address_from_derivation_path(&keychain, &parent_key_id, 0)?;
let sec_key =
address::address_from_derivation_path(&keychain, &parent_key_id, SlatepackAddressIndex(0))?;
let d_skey = DalekSecretKey::from_bytes(&sec_key.0);
let my_address_pubkey: DalekPublicKey = (&d_skey).into();
let possible_receiver_key: DalekPublicKey = (&d_skey).into();
let sender_mine = my_address_pubkey == sender_pubkey;
let recipient_mine = my_address_pubkey == recipient_pubkey;
let sec_key = address::address_from_derivation_path(
&keychain,
&parent_key_id,
proof
.sender_address_path
.unwrap_or(SlatepackAddressIndex(0)),
)?;
let d_skey = DalekSecretKey::from_bytes(&sec_key.0);
let possible_sender_key: DalekPublicKey = (&d_skey).into();
let sender_mine = possible_sender_key == sender_pubkey;
let recipient_mine = possible_receiver_key == recipient_pubkey;
Ok((sender_mine, recipient_mine))
}
@@ -1451,7 +1612,7 @@ where
})
}
/// Create MXMixnet request
/// Create mwixnet request
pub fn create_mwixnet_req<C, K>(
w: &mut WalletBackend<C, K>,
keychain_mask: Option<&SecretKey>,
@@ -1459,43 +1620,57 @@ pub fn create_mwixnet_req<C, K>(
commitment: &Commitment,
lock_output: bool,
use_test_rng: bool,
) -> Result<SwapReq, Error>
) -> Result<MwixnetReqCreationResult, Error>
where
C: NodeClient,
K: Keychain,
{
let parent_key_id = w.parent_key_id();
let keychain = w.keychain(keychain_mask)?;
let outputs = updater::retrieve_outputs(w, keychain_mask, false, None, Some(&parent_key_id))?;
let mut output = None;
for o in &outputs {
if o.commit == *commitment {
output = Some(o.output.clone());
break;
}
if params.server_keys.is_empty() {
return Err(Error::GenericError(
"mwixnet requires at least one server key".to_string(),
));
}
if output.is_none() {
return Err(Error::GenericError(String::from("output not found")));
}
let amount = output.clone().unwrap().value;
let input_blind = keychain.derive_key(
amount,
&output.clone().unwrap().key_id,
SwitchCommitmentType::Regular,
)?;
let mut server_pubkeys = vec![];
for i in 0..params.server_keys.len() {
server_pubkeys.push(xPublicKey::from(&StaticSecret::from(
params.server_keys[i].0,
if params.server_keys.len() > MAX_MWIXNET_HOPS {
return Err(Error::GenericError(format!(
"mwixnet supports at most {} server keys",
MAX_MWIXNET_HOPS
)));
}
let fee = grin_core::libtx::tx_fee(1, 1, 1);
let new_amount = amount - (fee * server_pubkeys.len() as u64);
let parent_key_id = w.parent_key_id();
let keychain = w.keychain(keychain_mask)?;
let outputs = updater::retrieve_outputs(w, keychain_mask, true, None, Some(&parent_key_id))?;
let output = outputs
.into_iter()
.find(|o| o.commit == *commitment)
.map(|o| o.output)
.ok_or_else(|| Error::GenericError("output not found".to_string()))?;
let current_height = w.w2n_client().get_chain_tip()?.0;
if !output.eligible_to_spend(current_height, 1) {
return Err(Error::GenericError("output is not spendable".to_string()));
}
let amount = output.value;
let input_blind = keychain.derive_key(amount, &output.key_id, SwitchCommitmentType::Regular)?;
let server_pubkeys = &params.server_keys;
let minimum_fee = TransactionBody::weight_by_iok(1, 1, 1) * DEFAULT_ACCEPT_FEE_BASE;
let fee_per_hop = params.fee_per_hop.unwrap_or(minimum_fee);
if fee_per_hop < minimum_fee {
return Err(Error::Fee(format!(
"mwixnet fee is below the swap minimum {}",
amount_to_hr_string(minimum_fee, true)
)));
}
let fee = FeeFields::try_from(fee_per_hop).map_err(|e| Error::Fee(e.to_string()))?;
let total_fee = fee_per_hop
.checked_mul(server_pubkeys.len() as u64)
.ok_or_else(|| Error::Fee("mwixnet fee overflow".to_string()))?;
let total_fee_fields = FeeFields::try_from(total_fee)
.map_err(|_| Error::Fee("mwixnet total fee exceeds FeeFields limit".to_string()))?;
let new_amount = amount
.checked_sub(total_fee)
.ok_or_else(|| Error::Fee("mwixnet fees exceed output value".to_string()))?;
let new_output = build_output(w, keychain_mask, OutputFeatures::Plain, new_amount)?;
let secp = keychain.secp();
@@ -1506,48 +1681,74 @@ where
let hops: Vec<Hop> = server_pubkeys
.iter()
.enumerate()
.map(|(i, &p)| {
.map(|(i, &p)| -> Result<Hop, Error> {
if (i + 1) == server_pubkeys.len() {
Hop {
Ok(Hop {
server_pubkey: p,
excess: blind_sum.secret_key(secp).unwrap(),
fee: FeeFields::from(fee as u32),
excess: blind_sum.secret_key(secp)?,
fee,
rangeproof: Some(new_output.output.proof),
}
})
} else {
let hop_excess = if use_test_rng {
BlindingFactor::zero()
} else {
BlindingFactor::rand(secp)
};
blind_sum = blind_sum.split(&hop_excess, secp).unwrap();
Hop {
blind_sum = blind_sum.split(&hop_excess, secp)?;
Ok(Hop {
server_pubkey: p,
excess: hop_excess.secret_key(secp).unwrap(),
fee: FeeFields::from(fee as u32),
excess: hop_excess.secret_key(secp)?,
fee,
rangeproof: None,
}
})
}
})
.collect();
.collect::<Result<Vec<_>, _>>()?;
let onion = create_onion(commitment, &hops, use_test_rng).unwrap();
let comsig = ComSignature::sign(
amount,
&input_blind,
&onion.serialize().unwrap(),
use_test_rng,
)
.unwrap();
let onion = create_onion(commitment, &hops, use_test_rng)
.map_err(|e| Error::GenericError(e.to_string()))?;
let onion_bytes = onion.serialize().map_err(Error::Deser)?;
let comsig = ComSignature::sign(amount, &input_blind, &onion_bytes, use_test_rng)
.map_err(|e| Error::Signature(e.to_string()))?;
// Lock output if requested
if lock_output {
let tx_id = if lock_output {
let mut batch = w.batch(keychain_mask)?;
let mut update_output = batch.get(&output.as_ref().unwrap().key_id, &None)?;
update_output.lock();
batch.lock_output(&mut update_output)?;
batch.commit()?;
}
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut tx = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id);
tx.amount_debited = amount;
tx.amount_credited = new_amount;
tx.num_inputs = 1;
tx.num_outputs = 1;
tx.fee = Some(total_fee_fields);
tx.kernel_lookup_min_height = Some(current_height);
Ok(SwapReq { comsig, onion })
let mut update_output = batch.get(&output.key_id, &output.mmr_index)?;
update_output.tx_log_entry = Some(log_id);
batch.lock_output(&mut update_output)?;
batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: new_output.key_id.clone(),
n_child: new_output.key_id.to_path().last_path_index(),
commit: Some(new_output.output.commitment().to_hex()),
mmr_index: None,
value: new_amount,
status: OutputStatus::Unconfirmed,
height: current_height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
})?;
batch.save_tx_log_entry(tx, &parent_key_id)?;
batch.commit()?;
Some(log_id)
} else {
None
};
Ok(MwixnetReqCreationResult {
request: SwapReq { comsig, onion },
tx_id,
})
}
+15 -5
View File
@@ -29,6 +29,7 @@ use grin_keychain::Keychain;
use grin_util::secp::SecretKey;
pub use crate::mwixnet::{Hop, MixnetReqCreationParams, SwapReq};
use crate::slatepack::SlatepackAddressIndex;
/// Type for storing amounts (in nanogrins).
/// Serializes as a string but can deserialize from a string or u64.
@@ -38,7 +39,7 @@ pub struct Amount(#[serde(with = "secp_ser::string_or_u64")] pub u64);
/// V2 Init / Send TX API Args
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InitTxArgs {
/// The human readable account name from which to draw outputs
/// The human-readable account name from which to draw outputs
/// for the transaction, overriding whatever the active account is as set via the
/// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method.
pub src_acct_name: Option<String>,
@@ -91,6 +92,9 @@ pub struct InitTxArgs {
/// inputs until just before finalization
#[serde(default)]
pub late_lock: Option<bool>,
/// Slatepack address index
#[serde(default)]
pub address_index: Option<SlatepackAddressIndex>,
/// Sender arguments. If present, the underlying function will also attempt to send the
/// transaction to a destination and optionally finalize the result
pub send_args: Option<InitTxSendArgs>,
@@ -126,6 +130,7 @@ impl Default for InitTxArgs {
estimate_only: Some(false),
payment_proof_recipient_address: None,
late_lock: Some(false),
address_index: None,
send_args: None,
}
}
@@ -134,7 +139,7 @@ impl Default for InitTxArgs {
/// V2 Issue Invoice Tx Args
#[derive(Clone, Serialize, Deserialize, Default)]
pub struct IssueInvoiceTxArgs {
/// The human readable account name to which the received funds should be added
/// The human-readable account name to which the received funds should be added
/// overriding whatever the active account is as set via the
/// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method.
pub dest_acct_name: Option<String>,
@@ -145,6 +150,8 @@ pub struct IssueInvoiceTxArgs {
/// down to the minimum slate version compatible with the current. If `None` the slate
/// is generated with the latest version.
pub target_slate_version: Option<u16>,
/// Issuer slatepack address derivation path index.
pub address_index: Option<SlatepackAddressIndex>,
}
/// Sort tx retrieval order
@@ -179,7 +186,7 @@ pub struct RetrieveTxQueryArgs {
/// Retrieve transactions with an id higher than or equal to the given
/// If None, consider items from the first transaction and later
pub min_id: Option<u32>,
/// Retrieve tranactions with an id less than or equal to the given
/// Retrieve transactions with an id less than or equal to the given
/// If None, consider items from the last transaction and earlier
pub max_id: Option<u32>,
/// The maximum number of transactions to return
@@ -216,7 +223,7 @@ pub struct RetrieveTxQueryArgs {
pub min_confirmed_timestamp: Option<DateTime<Utc>>,
/// higher bound on the confirmation timestamp, inclusive
pub max_confirmed_timestamp: Option<DateTime<Utc>>,
/// Field within the tranasction list on which to sort
/// Field within the transaction list on which to sort
/// defaults to ID if not present
pub sort_field: Option<RetrieveTxQuerySortField>,
/// Sort order, defaults to ASC if not present (earliest is first)
@@ -268,7 +275,7 @@ impl BlockFees {
}
}
/// Map Outputdata to commits
/// Map [OutputData] to commits
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OutputCommitMapping {
/// Output Data
@@ -324,6 +331,9 @@ pub struct PaymentProof {
/// Sender Signature
#[serde(with = "dalek_ser::dalek_sig_serde")]
pub sender_sig: DalekSignature,
/// Sender Slatepack address derivation path index
#[serde(default)]
pub sender_address_path: Option<SlatepackAddressIndex>,
}
/// Build output result
+62 -5
View File
@@ -21,6 +21,7 @@ use uuid::Uuid;
use crate::blake2::blake2b::{Blake2b, Blake2bResult};
use crate::slatepack::SlatepackAddressIndex;
use crate::{
AcctPathMapping, Context, Error, NodeClient, OutputData, ScannedBlockInfo, TxLogEntry,
WalletInitStatus,
@@ -44,19 +45,21 @@ const CONFIRMED_HEIGHT_PREFIX: u8 = b'c';
const PRIVATE_TX_CONTEXT_PREFIX: u8 = b'p';
const TX_LOG_ENTRY_PREFIX: u8 = b't';
const TX_LOG_ID_PREFIX: u8 = b'i';
const TX_ADDRESS_INDEX_PREFIX: u8 = b's';
const ACCOUNT_PATH_MAPPING_PREFIX: u8 = b'a';
const LAST_SCANNED_BLOCK: u8 = b'l';
const LAST_SCANNED_KEY: &str = "LAST_SCANNED_KEY";
const WALLET_INIT_STATUS: u8 = b'w';
const WALLET_INIT_STATUS_KEY: &str = "WALLET_INIT_STATUS";
const DB_PREFIXES: [u8; 9] = [
const DB_PREFIXES: [u8; 10] = [
OUTPUT_PREFIX,
DERIV_PREFIX,
CONFIRMED_HEIGHT_PREFIX,
PRIVATE_TX_CONTEXT_PREFIX,
TX_LOG_ENTRY_PREFIX,
TX_LOG_ID_PREFIX,
TX_ADDRESS_INDEX_PREFIX,
ACCOUNT_PATH_MAPPING_PREFIX,
LAST_SCANNED_BLOCK,
WALLET_INIT_STATUS,
@@ -369,10 +372,16 @@ where
}
/// Get an (Optional) tx log entry by uuid.
pub fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
self.db
.get_ser(Some(TX_LOG_ENTRY_PREFIX), u.as_bytes(), None)
.map_err(|e| e.into())
pub fn get_tx_log_entry(
&self,
id: &Uuid,
parent_id: &Identifier,
) -> Result<Option<TxLogEntry>, Error> {
let tx = self
.tx_log_iter()?
.flatten()
.find(|tx| tx.parent_key_id == *parent_id && tx.tx_slate_id == Some(*id));
Ok(tx)
}
/// Iterate over all tx log data stored by the backend.
@@ -392,6 +401,54 @@ where
.map_err(From::from)
}
/// Next transaction slatepack address derivation path index.
pub fn next_tx_slatepack_address_index(
&mut self,
parent_key_id: &Identifier,
) -> Result<SlatepackAddressIndex, Error> {
let mut index = {
let batch = self.db.batch()?;
batch
.get_ser(
Some(TX_ADDRESS_INDEX_PREFIX),
&parent_key_id.to_bytes(),
None,
)?
.unwrap_or(0)
};
if index >= SlatepackAddressIndex::MAX {
return Err(Error::SlatepackAddress(
"Slatepack address indices exhausted".into(),
));
}
index += 1;
let mut batch = self.db.batch()?;
batch.put_ser(
Some(TX_ADDRESS_INDEX_PREFIX),
&parent_key_id.to_bytes(),
&index,
)?;
batch.commit()?;
Ok(SlatepackAddressIndex(index))
}
/// Iterator over private tx contexts.
pub fn private_context_iter(
&self,
) -> Result<impl Iterator<Item = Result<Context, grin_store::Error>>, Error> {
let protocol_version = self.db.protocol_version();
self.db
.iter(Some(PRIVATE_TX_CONTEXT_PREFIX), move |_, mut v| {
ser::deserialize(
&mut v,
protocol_version,
ser::DeserializationMode::default(),
)
.map_err(From::from)
})
.map_err(From::from)
}
/// Retrieve the private context associated with a given slate id.
pub fn get_private_context(
&mut self,
+1
View File
@@ -208,6 +208,7 @@ where
sender_signature: None,
});
};
t.address_index = context.payment_proof_derivation_index;
// write the output representing our change
for (id, _, _) in &context.get_outputs() {
+7 -4
View File
@@ -20,6 +20,7 @@ use uuid::Uuid;
use crate::internal::{selection, updater};
use crate::slate::Slate;
use crate::slatepack::SlatepackAddressIndex;
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType};
use crate::util::OnionV3Address;
use crate::{address, Error};
@@ -368,7 +369,7 @@ where
Ok(())
}
/// Update the stored transaction (this update needs to happen when the TX is finalised)
/// Update the stored transaction (this update needs to happen when the TX is finalized)
pub fn update_stored_tx<C, K>(
wallet: &mut WalletBackend<C, K>,
keychain_mask: Option<&SecretKey>,
@@ -405,19 +406,21 @@ where
}
if let Some(ref p) = slate.clone().payment_proof {
let derivation_index = context.payment_proof_derivation_index.unwrap_or(0);
let sender_address_path = context
.payment_proof_derivation_index
.unwrap_or(SlatepackAddressIndex(0));
let keychain = wallet.keychain(keychain_mask)?;
let parent_key_id = wallet.parent_key_id();
let excess = slate.calc_excess(keychain.secp())?;
let sender_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
address::address_from_derivation_path(&keychain, &parent_key_id, sender_address_path)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
let sig =
create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?;
tx.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
sender_address_path: derivation_index,
sender_address_path,
sender_address: sender_address.to_ed25519()?,
sender_signature: Some(sig),
})
+1 -3
View File
@@ -57,9 +57,7 @@ pub use crate::slate_versions::{
SlateVersion, VersionedBinSlate, VersionedCoinbase, VersionedSlate, CURRENT_SLATE_VERSION,
GRIN_BLOCK_HEADER_VERSION,
};
pub use crate::slatepack::{
Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin, Slatepacker, SlatepackerArgs,
};
pub use crate::slatepack::{Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin};
pub use api_impl::owner_updater::StatusMessage;
pub use api_impl::types::{
Amount, BlockFees, BuiltOutput, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs,
+7 -2
View File
@@ -14,11 +14,16 @@
//! Onion modules for mxmixnet
pub mod onion;
mod response;
mod types;
pub use onion::{
create_onion, onion::Onion, onion::OnionError, util as onion_util, ComSigError, ComSignature,
MwixnetPublicKey,
MAX_MWIXNET_HOPS,
};
pub use types::{Hop, MixnetReqCreationParams, SwapReq};
pub use response::{parse_mwixnet_response, MwixnetResponse, MwixnetResponseError};
pub use types::{
Hop, MixnetReqCreationParams, MwixnetReqCreationResult, MwixnetServerPublicKey, SwapReq,
};
+30 -24
View File
@@ -36,48 +36,51 @@ pub enum DalekError {
SigVerifyFailed,
}
/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization
/// Ed25519 server identity key
#[derive(Clone, Debug, PartialEq)]
pub struct DalekPublicKey(VerifyingKey);
pub struct MwixnetServerIdentityKey(VerifyingKey);
impl DalekPublicKey {
/// Convert DalekPublicKey to hex string
impl MwixnetServerIdentityKey {
/// Convert the identity key to hex
pub fn to_hex(&self) -> String {
self.0.to_hex()
}
/// Convert hex string to DalekPublicKey.
/// Parse an identity key from hex
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
let err = DalekError::HexError(format!("failed to decode {}", hex));
let bytes = grin_util::from_hex(hex).map_err(|_| err.clone())?;
let b = <&[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| err.clone())?;
let pk = VerifyingKey::from_bytes(b).map_err(|_| err)?;
Ok(DalekPublicKey(pk))
Ok(MwixnetServerIdentityKey(pk))
}
/// Compute DalekPublicKey from a SecretKey
/// Derive the identity key from a secret key
pub fn from_secret(key: &SecretKey) -> Self {
let secret = ed25519_dalek::SigningKey::from_bytes(&key.0);
let pk: VerifyingKey = (&secret).into();
DalekPublicKey(pk)
MwixnetServerIdentityKey(pk)
}
}
impl AsRef<VerifyingKey> for DalekPublicKey {
impl AsRef<VerifyingKey> for MwixnetServerIdentityKey {
fn as_ref(&self) -> &VerifyingKey {
&self.0
}
}
/// Serializes an Option<DalekPublicKey> to and from hex
/// Serialize an optional identity key as hex
pub mod option_dalek_pubkey_serde {
use super::DalekPublicKey;
use super::MwixnetServerIdentityKey;
use grin_util::ToHex;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
/// Write the Ed25519 public key as hex, or null
pub fn serialize<S>(pk: &Option<DalekPublicKey>, serializer: S) -> Result<S::Ok, S::Error>
pub fn serialize<S>(
pk: &Option<MwixnetServerIdentityKey>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -88,29 +91,31 @@ pub mod option_dalek_pubkey_serde {
}
/// Read an optional Ed25519 public key from hex
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DalekPublicKey>, D::Error>
pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<MwixnetServerIdentityKey>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).and_then(|res| match res {
Some(string) => DalekPublicKey::from_hex(&string)
Some(string) => MwixnetServerIdentityKey::from_hex(&string)
.map_err(|e| Error::custom(e.to_string()))
.map(|pk: DalekPublicKey| Some(pk)),
.map(|pk: MwixnetServerIdentityKey| Some(pk)),
None => Ok(None),
})
}
}
impl Readable for DalekPublicKey {
impl Readable for MwixnetServerIdentityKey {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let bytes = reader.read_fixed_bytes(32)?;
let b = <&[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| ser::Error::CorruptedData)?;
let pk = VerifyingKey::from_bytes(b).map_err(|_| ser::Error::CorruptedData)?;
Ok(DalekPublicKey(pk))
Ok(MwixnetServerIdentityKey(pk))
}
}
impl Writeable for DalekPublicKey {
impl Writeable for MwixnetServerIdentityKey {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.0.to_bytes())?;
Ok(())
@@ -136,7 +141,7 @@ impl DalekSignature {
/// Verifies DalekSignature
#[allow(dead_code)]
pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> {
pub fn verify(&self, pk: &MwixnetServerIdentityKey, msg: &[u8]) -> Result<(), DalekError> {
pk.as_ref()
.verify(msg, &self.0)
.map_err(|_| DalekError::SigVerifyFailed)
@@ -196,20 +201,21 @@ mod tests {
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
struct TestPubKeySerde {
#[serde(with = "option_dalek_pubkey_serde", default)]
pk: Option<DalekPublicKey>,
pk: Option<MwixnetServerIdentityKey>,
}
#[test]
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
// Test from_hex
let rand_pk = rand_keypair().1;
let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap();
let pk_from_hex = MwixnetServerIdentityKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap();
assert_eq!(rand_pk.0, pk_from_hex.0);
// Test ser (de-)serialization
let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap();
assert_eq!(bytes.len(), 32);
let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap();
let pk_from_deser: MwixnetServerIdentityKey =
ser::deserialize_default(&mut &bytes[..]).unwrap();
assert_eq!(rand_pk.0, pk_from_deser.0);
// Test serde with Some(rand_pk)
@@ -260,7 +266,7 @@ mod tests {
// Sign a message
let s = Secp256k1::new();
let sk = SecretKey::from_slice(&s, &[1; 32]).unwrap();
let pk = DalekPublicKey::from_secret(&sk);
let pk = MwixnetServerIdentityKey::from_secret(&sk);
let msg: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let sig = sign(&sk, &msg).unwrap();
@@ -274,7 +280,7 @@ mod tests {
// Wrong pubkey
let wrong_sk = SecretKey::from_slice(&s, &[2; 32]).unwrap();
let wrong_pk = DalekPublicKey::from_secret(&wrong_sk);
let wrong_pk = MwixnetServerIdentityKey::from_secret(&wrong_sk);
assert!(sig.verify(&wrong_pk, &msg).is_err());
// Test from_hex
+110 -17
View File
@@ -19,9 +19,7 @@ pub mod crypto;
pub mod onion;
pub mod util;
pub use crypto::{
comsig_serde, dalek::DalekPublicKey as MwixnetPublicKey, ComSigError, ComSignature,
};
pub use crypto::{comsig_serde, ComSigError, ComSignature};
use chacha20::cipher::StreamCipher;
use grin_core::core::FeeFields;
@@ -32,14 +30,18 @@ use grin_util::secp::{
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::{SharedSecret, StaticSecret};
use crate::mwixnet::MwixnetServerPublicKey;
use crypto::secp::random_secret;
use onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes};
/// Maximum route length, including the swap server.
pub const MAX_MWIXNET_HOPS: usize = 8;
/// Onion hop struct
#[derive(Clone)]
pub struct Hop {
/// Comsig server public key
pub server_pubkey: xPublicKey,
pub server_pubkey: MwixnetServerPublicKey,
/// Kernel excess
pub excess: SecretKey,
/// Fee
@@ -57,7 +59,7 @@ pub fn new_hop(
proof: Option<RangeProof>,
) -> Hop {
Hop {
server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0)),
server_pubkey: MwixnetServerPublicKey::from_secret(server_key),
excess: hop_excess.clone(),
fee: FeeFields::from(fee),
rangeproof: proof,
@@ -70,6 +72,11 @@ pub fn create_onion(
hops: &[Hop],
use_test_rng: bool,
) -> Result<Onion, OnionError> {
if hops.len() > MAX_MWIXNET_HOPS {
return Err(OnionError::TooManyHops {
max: MAX_MWIXNET_HOPS,
});
}
if hops.is_empty() {
return Ok(Onion {
ephemeral_pubkey: xPublicKey::from([0u8; 32]),
@@ -84,7 +91,10 @@ pub fn create_onion(
let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk);
for i in 0..hops.len() {
let hop = &hops[i];
let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey);
let shared_secret = ephemeral_sk.diffie_hellman(&xPublicKey::from(hop.server_pubkey.0));
if !shared_secret.was_contributory() {
return Err(OnionError::NonContributorySharedSecret);
}
shared_secrets.push(shared_secret);
ephemeral_sk = StaticSecret::from(random_secret(use_test_rng).0);
@@ -123,7 +133,7 @@ pub fn create_onion(
#[cfg(any(test, feature = "mwixnet-test"))]
pub mod test_util {
use super::*;
use crypto::dalek::DalekPublicKey;
use crypto::dalek::MwixnetServerIdentityKey;
use crypto::secp;
use grin_core::core::hash::Hash;
@@ -154,20 +164,20 @@ pub mod test_util {
}
pub fn rand_commit() -> Commitment {
secp::commit(rand::thread_rng().next_u64(), &secp::random_secret(false)).unwrap()
secp::commit(thread_rng().next_u64(), &random_secret(false)).unwrap()
}
pub fn rand_hash() -> Hash {
Hash::from_hex(secp::random_secret(false).to_hex().as_str()).unwrap()
Hash::from_hex(random_secret(false).to_hex().as_str()).unwrap()
}
pub fn rand_proof() -> RangeProof {
let secp = Secp256k1::new();
secp.bullet_proof(
rand::thread_rng().next_u64(),
secp::random_secret(false),
secp::random_secret(false),
secp::random_secret(false),
thread_rng().next_u64(),
random_secret(false),
random_secret(false),
random_secret(false),
None,
None,
)
@@ -193,8 +203,8 @@ pub mod test_util {
.bullet_proof(
out_value,
blind.clone(),
secp::random_secret(false),
secp::random_secret(false),
random_secret(false),
random_secret(false),
None,
None,
)
@@ -203,9 +213,92 @@ pub mod test_util {
(secp::commit(out_value, &blind).unwrap(), rp)
}
pub fn rand_keypair() -> (SecretKey, DalekPublicKey) {
pub fn rand_keypair() -> (SecretKey, MwixnetServerIdentityKey) {
let sk = random_secret(false);
let pk = DalekPublicKey::from_secret(&sk);
let pk = MwixnetServerIdentityKey::from_secret(&sk);
(sk, pk)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mwixnet::MwixnetServerPublicKey;
#[test]
fn rejects_zero_key() {
let commitment = test_util::rand_commit();
let hop = Hop {
server_pubkey: MwixnetServerPublicKey([0u8; 32]),
excess: random_secret(false),
fee: FeeFields::from(1u32),
rangeproof: None,
};
assert_eq!(
create_onion(&commitment, &[hop], false),
Err(OnionError::NonContributorySharedSecret)
);
}
#[test]
fn rejects_too_many_hops() {
let commitment = test_util::rand_commit();
let hops: Vec<Hop> = (0..=MAX_MWIXNET_HOPS)
.map(|_| new_hop(&random_secret(false), &random_secret(false), 1, None))
.collect();
assert!(create_onion(&commitment, &hops[..MAX_MWIXNET_HOPS], false).is_ok());
assert_eq!(
create_onion(&commitment, &hops, false),
Err(OnionError::TooManyHops {
max: MAX_MWIXNET_HOPS
})
);
}
#[test]
fn x25519_key_roundtrip() {
let server_key = SecretKey::from_slice(
&grin_util::secp::Secp256k1::new(),
&grin_util::from_hex(
"a129111d283b13bf93957c06bf6605c3417b4b89db4b5cb2e7dab2c15e36e0a4",
)
.unwrap(),
)
.unwrap();
let public_key = MwixnetServerPublicKey::from_secret(&server_key);
assert_eq!(
public_key.to_hex(),
"96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b"
);
let identity_key =
crypto::dalek::MwixnetServerIdentityKey::from_secret(&server_key).to_hex();
assert_ne!(identity_key, public_key.to_hex());
let commitment = crypto::secp::commit(1_000, &server_key).unwrap();
let excess = server_key.clone();
let hop = Hop {
server_pubkey: public_key,
excess: excess.clone(),
fee: FeeFields::from(1u32),
rangeproof: None,
};
let onion = create_onion(&commitment, &[hop], true).unwrap();
let peeled = onion.peel_layer(&server_key).unwrap();
assert_eq!(peeled.payload.excess, excess);
assert_eq!(peeled.payload.fee, FeeFields::from(1u32));
let identity_key = MwixnetServerPublicKey::from_hex(&identity_key).unwrap();
let wrong_hop = Hop {
server_pubkey: identity_key,
excess: server_key.clone(),
fee: FeeFields::from(1u32),
rangeproof: None,
};
let wrong_onion = create_onion(&commitment, &[wrong_hop], true).unwrap();
assert!(wrong_onion.peel_layer(&server_key).is_err());
}
}
+9
View File
@@ -336,6 +336,15 @@ pub enum OnionError {
/// Invalid Key Length
#[error("Invalid key length for MAC initialization")]
InvalidKeyLength,
/// Non-contributory X25519 shared secret
#[error("Non-contributory X25519 shared secret")]
NonContributorySharedSecret,
/// Too many hops
#[error("Mwixnet routes support at most {max} hops")]
TooManyHops {
/// Maximum supported number of hops
max: usize,
},
/// Serialization Error
#[error("Serialization error occurred: {0:?}")]
SerializationError(ser::Error),
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2026 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Mwixnet response parsing.
use serde::Deserialize;
use serde_json::{self, Value};
/// Result returned by a mwixnet server after submitting a swap request.
#[derive(Debug, Eq, PartialEq)]
pub enum MwixnetResponse {
/// The swap request was accepted.
Accepted,
/// The swap request was rejected with a message.
//TODO: extend it with actual errors from mwixnet
Rejected(String),
}
/// Error parsing or validating a mwixnet response.
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
#[error("{0}")]
pub struct MwixnetResponseError(String);
#[derive(Deserialize)]
struct JsonRpcResponse {
result: Option<Value>,
error: Option<JsonRpcError>,
id: Value,
jsonrpc: Option<String>,
}
#[derive(Deserialize)]
struct JsonRpcError {
#[serde(rename = "code")]
_code: i32,
message: String,
}
/// Parse and validate the JSON-RPC response returned by a mwixnet server.
pub fn parse_mwixnet_response(response: &str) -> Result<MwixnetResponse, MwixnetResponseError> {
let response: JsonRpcResponse = serde_json::from_str(response)
.map_err(|e| MwixnetResponseError(format!("Invalid mwixnet response: {}", e)))?;
if response.jsonrpc.as_deref() != Some("2.0") {
return Err(MwixnetResponseError(
"Invalid mwixnet response version".to_string(),
));
}
if response.id != serde_json::json!(1) {
return Err(MwixnetResponseError(
"Invalid mwixnet response ID".to_string(),
));
}
if response.result.is_some() && response.error.is_some() {
return Err(MwixnetResponseError(
"Mwixnet response contains both result and error".to_string(),
));
}
if let Some(error) = response.error {
return Ok(MwixnetResponse::Rejected(error.message));
}
if response.result == Some(serde_json::json!("success")) {
return Ok(MwixnetResponse::Accepted);
}
Err(MwixnetResponseError(format!(
"Unexpected mwixnet response result: {:?}",
response.result
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_mwixnet_response() {
assert_eq!(
parse_mwixnet_response(r#"{"jsonrpc":"2.0","result":"success","id":1}"#).unwrap(),
MwixnetResponse::Accepted
);
assert_eq!(
parse_mwixnet_response(
r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"invalid swap"},"id":1}"#,
)
.unwrap(),
MwixnetResponse::Rejected("invalid swap".to_string())
);
assert!(parse_mwixnet_response(r#"{"jsonrpc":"2.0","result":"success","id":2}"#).is_err());
assert!(parse_mwixnet_response(r#"{"jsonrpc":"1.0","result":"success","id":1}"#).is_err());
}
}
+67 -9
View File
@@ -13,12 +13,15 @@
// limitations under the License.
//! Types related to mwixnet requests required by rest of lib crate apis
//! Should rexport all needed types here
//! Should export all needed types here
use super::onion::comsig_serde;
use grin_core::libtx::secp_ser::string_or_u64;
use grin_core::libtx::secp_ser::opt_string_or_u64;
use grin_util::secp::key::SecretKey;
use serde::{Deserialize, Serialize};
use grin_util::ToHex;
use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use x25519_dalek::{PublicKey, StaticSecret};
pub use super::onion::{onion::Onion, ComSignature, Hop};
@@ -32,12 +35,67 @@ pub struct SwapReq {
pub onion: Onion,
}
/// mwixnetRequest Creation Params
/// Result of creating a mwixnet request.
#[derive(Serialize, Deserialize, Debug)]
pub struct MwixnetReqCreationResult {
/// Request to submit to the first mwixnet server.
#[serde(flatten)]
pub request: SwapReq,
/// Transaction log ID when the wallet output was locked.
pub tx_id: Option<u32>,
}
/// Public X25519 key of an mwixnet server.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MwixnetServerPublicKey(pub(crate) [u8; 32]);
impl MwixnetServerPublicKey {
/// Derive the public key published by a mwixnet server.
pub fn from_secret(key: &SecretKey) -> Self {
let key = PublicKey::from(&StaticSecret::from(key.0));
Self(key.to_bytes())
}
/// Parse a public key from hexadecimal representation.
pub fn from_hex(value: &str) -> Result<Self, String> {
let bytes = grin_util::from_hex(value).map_err(|e| e.to_string())?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| "mwixnet server public key must be 32 bytes".to_string())?;
Ok(Self(bytes))
}
/// Return the hexadecimal representation.
pub fn to_hex(self) -> String {
self.0.to_hex()
}
}
impl Serialize for MwixnetServerPublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for MwixnetServerPublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::from_hex(&value).map_err(D::Error::custom)
}
}
/// Mwixnet request creation params
#[derive(Serialize, Deserialize, Debug)]
pub struct MixnetReqCreationParams {
/// List of all the server keys
pub server_keys: Vec<SecretKey>,
/// Fees per hop
#[serde(with = "string_or_u64")]
pub fee_per_hop: u64,
/// Ordered public keys of 1 to [`super::MAX_MWIXNET_HOPS`] participating servers
pub server_keys: Vec<MwixnetServerPublicKey>,
/// Optional fee per hop with the swap minimum as default
#[serde(default, with = "opt_string_or_u64")]
pub fee_per_hop: Option<u64>,
}
+23 -8
View File
@@ -16,6 +16,13 @@
//! around during an interactive wallet exchange
use crate::error::Error;
use crate::slate_versions::v4::{
CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4,
SlateStateV4, SlateV4, VersionCompatInfoV4,
};
use crate::slate_versions::VersionedSlate;
use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION};
use crate::{Context, SlateVersion, Slatepack, SlatepackAddress, VersionedBinSlate};
use ed25519_dalek::Signature as DalekSignature;
use ed25519_dalek::VerifyingKey as DalekPublicKey;
use grin_core::core::amount_to_hr_string;
@@ -30,18 +37,11 @@ use grin_util::secp::key::{PublicKey, SecretKey};
use grin_util::secp::pedersen::Commitment;
use grin_util::secp::Signature;
use grin_util::{secp, static_secp_instance};
use grin_wallet_util::byte_ser;
use serde::ser::{Serialize, Serializer};
use std::fmt;
use uuid::Uuid;
use crate::slate_versions::v4::{
CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4,
SlateStateV4, SlateV4, VersionCompatInfoV4,
};
use crate::slate_versions::VersionedSlate;
use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION};
use crate::Context;
#[derive(Debug, Clone)]
pub struct PaymentInfo {
/// Sender address
@@ -670,6 +670,21 @@ impl Slate {
Ok(())
}
}
/// Create slatepack from this slate.
pub fn create_slatepack(
&self,
sender: Option<SlatepackAddress>,
recipients: Vec<SlatepackAddress>,
) -> Result<Slatepack, Error> {
let out_slate = VersionedSlate::into_version(self.clone(), SlateVersion::V4)?;
let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?;
let mut slatepack = Slatepack::default();
slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?;
slatepack.sender = sender;
slatepack.try_encrypt_payload(recipients)?;
Ok(slatepack)
}
}
impl Serialize for Slate {
+4 -2
View File
@@ -29,9 +29,11 @@ use std::str;
use super::types::{Slatepack, SlatepackBin};
// Framing and formatting for slate armor
/// Slatepack file header.
pub static HEADER: &str = "BEGINSLATEPACK.";
static FOOTER: &str = ". ENDSLATEPACK.";
/// Slatepack file footer.
pub static FOOTER: &str = ". ENDSLATEPACK.";
const WORD_LENGTH: usize = 15;
const WORDS_PER_LINE: usize = 200;
const WEIGHT_RATIO: u64 = 32;
+25 -4
View File
@@ -16,10 +16,31 @@
mod address;
mod armor;
mod packer;
mod types;
pub use self::address::SlatepackAddress;
pub use self::armor::{max_size, min_size, SlatepackArmor};
pub use self::packer::{Slatepacker, SlatepackerArgs};
pub use self::types::{Slatepack, SlatepackBin};
pub use self::armor::{max_size, min_size, SlatepackArmor, FOOTER, HEADER};
pub use self::types::{Slatepack, SlatepackAddressIndex, SlatepackBin};
use std::fs::{metadata, File};
use std::io::Read;
use std::path::PathBuf;
/// Get slatepack file contents.
pub fn get_slatepack_file_contents(path: &PathBuf) -> Result<Vec<u8>, crate::Error> {
let metadata = metadata(path)?;
let len = metadata.len();
let min_len = min_size();
let max_len = max_size();
if len < min_len || len > max_len {
let msg = format!(
"Data is invalid length: {} | min: {}, max: {} |",
len, min_len, max_len
);
return Err(crate::Error::SlatepackDeser(msg));
}
let mut pub_tx_f = File::open(path)?;
let mut data = Vec::new();
pub_tx_f.read_to_end(&mut data)?;
Ok(data)
}
-121
View File
@@ -1,121 +0,0 @@
// Copyright 2021 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryFrom;
use std::str;
use super::armor::HEADER;
use crate::Error;
use crate::{
slatepack, Slate, SlateVersion, Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin,
VersionedBinSlate, VersionedSlate,
};
use grin_wallet_util::byte_ser;
use ed25519_dalek::SigningKey as edSecretKey;
#[derive(Clone)]
/// Arguments, mostly for encrypting decrypting a slatepack
pub struct SlatepackerArgs<'a> {
/// Optional sender to include in slatepack
pub sender: Option<SlatepackAddress>,
/// Optional list of recipients, for encryption
pub recipients: Vec<SlatepackAddress>,
/// Optional decryption key
pub dec_key: Option<&'a edSecretKey>,
}
/// Helper struct to pack and unpack slatepacks
#[derive(Clone)]
pub struct Slatepacker<'a>(SlatepackerArgs<'a>);
impl<'a> Slatepacker<'a> {
/// Create with pathbuf and recipients
pub fn new(args: SlatepackerArgs<'a>) -> Self {
Self(args)
}
/// return slatepack
pub fn deser_slatepack(&self, data: &[u8], decrypt: bool) -> Result<Slatepack, Error> {
// check if data is armored, if so, remove and continue
let data_len = data.len() as u64;
if data_len < slatepack::min_size() || data_len > slatepack::max_size() {
let msg = "Data invalid length".to_string();
return Err(Error::SlatepackDeser(msg));
}
let test_header = &data[..HEADER.len()];
let data = match str::from_utf8(test_header) {
Ok(s) => {
if s == HEADER {
SlatepackArmor::decode(data)?
} else {
data.to_vec()
}
}
Err(_) => data.to_vec(),
};
// try as bin first, then as json
let mut slatepack = match byte_ser::from_bytes::<SlatepackBin>(&data) {
Ok(s) => s.0,
Err(e) => {
debug!("Not a valid binary slatepack: {} - Will try JSON", e);
let content = String::from_utf8(data).map_err(|e| {
let msg = format!("{}", e);
Error::SlatepackDeser(msg)
})?;
serde_json::from_str(&content).map_err(|e| {
let msg = format!("Error reading JSON slatepack: {}", e);
Error::SlatepackDeser(msg)
})?
}
};
slatepack.ver_check_warn();
if decrypt {
slatepack.try_decrypt_payload(self.0.dec_key)?;
}
Ok(slatepack)
}
/// Create slatepack from slate and args
pub fn create_slatepack(&self, slate: &Slate) -> Result<Slatepack, Error> {
let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?;
let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?;
let mut slatepack = Slatepack::default();
slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?;
slatepack.sender = self.0.sender.clone();
slatepack.try_encrypt_payload(self.0.recipients.clone())?;
Ok(slatepack)
}
/// Armor a slatepack
pub fn armor_slatepack(&self, slatepack: &Slatepack) -> Result<String, Error> {
SlatepackArmor::encode(slatepack)
}
/// Return/upgrade slate from slatepack
pub fn get_slate(&self, slatepack: &Slatepack) -> Result<Slate, Error> {
let slate_bin =
byte_ser::from_bytes::<VersionedBinSlate>(&slatepack.payload).map_err(|e| {
error!("Error reading slate from armored slatepack: {}", e);
let msg = format!("{}", e);
Error::SlatepackDeser(msg)
})?;
Slate::upgrade(slate_bin.into())
}
}
+68 -12
View File
@@ -12,22 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use bech32::{self, ToBase32};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
/// Slatepack Types + Serialization implementation
use ed25519_dalek::SigningKey as edSecretKey;
use sha2::{Digest, Sha512};
use x25519_dalek::StaticSecret;
use crate::dalek_ser;
use super::SlatepackAddress;
use crate::Error;
use crate::{dalek_ser, Slate, SlatepackArmor, VersionedBinSlate};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use grin_wallet_util::byte_ser;
use super::SlatepackAddress;
use bech32::{self, ToBase32};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use ed25519_dalek::SigningKey as edSecretKey;
use sha2::{Digest, Sha512};
use std::fmt;
use std::io::{Cursor, Read, Write};
use x25519_dalek::StaticSecret;
pub const SLATEPACK_MAJOR_VERSION: u8 = 1;
pub const SLATEPACK_MINOR_VERSION: u8 = 0;
@@ -260,6 +257,52 @@ impl Slatepack {
);
}
}
/// Return/upgrade slate from slatepack.
pub fn get_slate(&self) -> Result<Slate, Error> {
if self.mode == 1 {
return Err(Error::SlatepackDecryption(
"Slatepack payload is encrypted".into(),
));
}
let slate_bin = byte_ser::from_bytes::<VersionedBinSlate>(&self.payload).map_err(|e| {
error!("Error reading slate from armored slatepack: {}", e);
let msg = format!("{}", e);
Error::SlatepackDeser(msg)
})?;
Slate::upgrade(slate_bin.into())
}
/// Armor this slatepack.
pub fn armor(&self) -> Result<String, Error> {
SlatepackArmor::encode(self)
}
}
/// Slatepack address derivation path index
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub struct SlatepackAddressIndex(pub u32);
impl SlatepackAddressIndex {
/// The max BIP32 index.
pub const MAX: u32 = 2_147_483_647;
/// Check the index range
pub fn check(self) -> Result<Self, Error> {
if self.0 > Self::MAX {
return Err(Error::SlatepackAddress(format!(
"Slatepack index {} is out of bound, maximum value is {}",
self.0,
Self::MAX
)));
}
Ok(self)
}
/// Index value.
pub fn value(self) -> u32 {
self.0
}
}
/// Wrapper for outputting slate as binary
@@ -335,7 +378,7 @@ impl Writeable for SlatepackBin {
};
// encrypted metadata is only included in the payload
// on encryption, and is not serialised here
// on encryption, and is not serialized here
// Now write payload (length prefixed)
writer.write_bytes(sp.payload.clone())
@@ -684,7 +727,7 @@ fn slatepack_bin_opt_fields_ser() -> Result<(), grin_wallet_util::byte_ser::Erro
// ensure that a slatepack with unknown data in the optional fields can be read
#[test]
fn slatepack_bin_future() -> Result<(), grin_wallet_util::byte_ser::Error> {
fn slatepack_bin_future() -> Result<(), byte_ser::Error> {
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use grin_core::global;
use grin_wallet_util::byte_ser;
@@ -908,3 +951,16 @@ fn slatepack_decrypt_rejects_malformed_plaintexts() -> Result<(), Error> {
Ok(())
}
#[test]
fn index_bounds() {
for index in [0, SlatepackAddressIndex::MAX] {
assert_eq!(SlatepackAddressIndex(index).check().unwrap().value(), index);
}
for index in [SlatepackAddressIndex::MAX + 1, u32::MAX] {
assert!(matches!(
SlatepackAddressIndex(index).check(),
Err(Error::SlatepackAddress(_))
));
}
}
+7 -2
View File
@@ -18,6 +18,7 @@
use crate::config::{TorConfig, WalletConfig};
use crate::error::Error;
use crate::slate_versions::ser as dalek_ser;
use crate::slatepack::SlatepackAddressIndex;
use crate::{InitTxArgs, SlateState, WalletBackend};
use chrono::prelude::*;
use ed25519_dalek::Signature as DalekSignature;
@@ -386,7 +387,7 @@ pub struct Context {
/// store the calculated fee
pub fee: Option<FeeFields>,
/// Payment proof sender address derivation path, if needed
pub payment_proof_derivation_index: Option<u32>,
pub payment_proof_derivation_index: Option<SlatepackAddressIndex>,
/// If late-locking, store my tranasction creation prefs
/// for later
pub late_lock_args: Option<InitTxArgs>,
@@ -675,6 +676,9 @@ pub struct TxLogEntry {
/// Track the time it took for a transaction to get reverted
#[serde(with = "option_duration_as_secs", default)]
pub reverted_after: Option<Duration>,
/// Slatepack address derivation path index
#[serde(default)]
pub address_index: Option<SlatepackAddressIndex>,
}
impl ser::Writeable for TxLogEntry {
@@ -713,6 +717,7 @@ impl TxLogEntry {
kernel_lookup_min_height: None,
payment_proof: None,
reverted_after: None,
address_index: None,
}
}
@@ -741,7 +746,7 @@ pub struct StoredProofInfo {
/// receiver signature
pub receiver_signature: Option<DalekSignature>,
/// sender address derivation path index
pub sender_address_path: u32,
pub sender_address_path: SlatepackAddressIndex,
/// sender address
#[serde(with = "dalek_ser::dalek_pubkey_serde")]
pub sender_address: DalekPublicKey,
+40
View File
@@ -185,6 +185,41 @@ 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
- mwixnet:
about: EXPERIMENTAL - Swap a wallet output through an mwixnet route using Arti only
args:
- output:
help: Commitment or minimum amount of the wallet output to swap, or 'max' for the largest eligible output
index: 1
required: true
- server:
help: Ed25519 identity key or .onion address of the first mwixnet server
long: server
takes_value: true
required: true
- key:
help: X25519 onion key of an mwixnet server, repeated in route order
long: key
takes_value: true
multiple: true
number_of_values: 1
required: true
- fee_per_hop:
help: Fee per mwixnet server in Grin, e.g. 0.0125, with the swap minimum as default
long: fee_per_hop
takes_value: true
required: false
- minimum_confirmations:
help: Minimum confirmations required for the selected output
short: c
long: min_conf
default_value: "10"
takes_value: true
- unpack:
about: Unpack and display an armored Slatepack Message, decrypting if possible
args:
@@ -264,6 +299,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:
+207 -68
View File
@@ -20,17 +20,22 @@ 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;
use grin_util::secp::constants::PEDERSEN_COMMITMENT_SIZE;
use grin_util::secp::pedersen::Commitment;
use grin_wallet_api::Owner;
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::mwixnet::{MixnetReqCreationParams, MwixnetServerPublicKey};
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 +43,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 +532,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 +553,90 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
skip_tor,
bridge,
slatepack_qr,
address_index,
})
}
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));
}
}
} else {
None
};
Ok(address)
}
fn parse_mwixnet_output(output: &str) -> Result<command::MwixnetOutput, ParseError> {
if output.eq_ignore_ascii_case("max") {
Ok(command::MwixnetOutput::Max)
} else if output.len() == PEDERSEN_COMMITMENT_SIZE * 2 {
let commitment = grin_util::from_hex(output)
.map_err(|e| ParseError::ArgumentError(format!("Invalid output commitment: {}", e)))?;
Ok(command::MwixnetOutput::Commitment(Commitment::from_vec(
commitment,
)))
} else {
let amount = core::core::amount_from_hr_string(output).map_err(|e| {
ParseError::ArgumentError(format!(
"Output must be a commitment, an amount, or 'max': {}",
e
))
})?;
Ok(command::MwixnetOutput::MinimumAmount(amount))
}
}
pub fn parse_mwixnet_args(args: &ArgMatches) -> Result<command::MwixnetArgs, ParseError> {
let output = parse_mwixnet_output(parse_required(args, "output")?)?;
let server = parse_required(args, "server")?
.try_into()
.map_err(|e| ParseError::ArgumentError(format!("Invalid mwixnet server: {}", e)))?;
let server_keys = args
.values_of("key")
.ok_or_else(|| {
ParseError::ArgumentError("At least one server key is required".to_string())
})?
.map(|key| {
MwixnetServerPublicKey::from_hex(key).map_err(|e| {
ParseError::ArgumentError(format!("Invalid mwixnet server key: {}", e))
})
})
.collect::<Result<Vec<_>, _>>()?;
let fee_per_hop = args
.value_of("fee_per_hop")
.map(core::core::amount_from_hr_string)
.transpose()
.map_err(|e| ParseError::ArgumentError(format!("Invalid fee_per_hop: {}", e)))?;
let minimum_confirmations = parse_u64(
parse_required(args, "minimum_confirmations")?,
"minimum_confirmations",
)?;
Ok(command::MwixnetArgs {
server,
output,
minimum_confirmations,
params: MixnetReqCreationParams {
server_keys,
fee_per_hop,
},
})
}
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);
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() {
@@ -590,8 +665,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 +678,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 +715,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 +770,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 +828,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 +847,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 +922,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 +938,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 +1021,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 +1119,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", _) => {
@@ -1230,6 +1293,10 @@ where
test_mode,
)
}
("mwixnet", Some(args)) => {
let a = arg_parse!(parse_mwixnet_args(args));
command::mwixnet(owner_api, km, a)
}
("receive", Some(args)) => {
let a = arg_parse!(parse_receive_args(args));
command::receive(owner_api, km, a, tor_config, test_mode)
@@ -1237,7 +1304,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));
@@ -1324,3 +1391,75 @@ where
}
}
}
#[cfg(test)]
mod mwixnet_tests {
use super::*;
#[test]
fn parses_mwixnet_optional_fee() {
let yaml = load_yaml!("../bin/grin-wallet.yml");
let args = vec![
"grin-wallet",
"mwixnet",
"max",
"--server",
"mza3u6vkqodqc6kjjapfbcjrg7hgpkuw6nyq5xvfjqp5jm2lfeaocwqd.onion",
"--key",
"24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a",
];
let matches = clap::App::from_yaml(yaml)
.get_matches_from_safe(args.clone())
.unwrap();
let parsed = parse_mwixnet_args(matches.subcommand_matches("mwixnet").unwrap()).unwrap();
assert_eq!(parsed.params.fee_per_hop, None);
let mut custom_args = args;
custom_args.extend(["--fee_per_hop", "0.0125"]);
let matches = clap::App::from_yaml(yaml)
.get_matches_from_safe(custom_args)
.unwrap();
let parsed = parse_mwixnet_args(matches.subcommand_matches("mwixnet").unwrap()).unwrap();
assert_eq!(parsed.params.fee_per_hop, Some(12_500_000));
}
#[test]
fn mwixnet_args() {
let yaml = load_yaml!("../bin/grin-wallet.yml");
let key = "24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a";
for keys in [vec!["--key", key], vec!["--key", key, "--key", key]] {
let key_count = keys.len() / 2;
let mut args = vec![
"grin-wallet",
"mwixnet",
"--server",
"mza3u6vkqodqc6kjjapfbcjrg7hgpkuw6nyq5xvfjqp5jm2lfeaocwqd.onion",
];
args.extend(keys);
args.push("max");
let matches = clap::App::from_yaml(yaml)
.get_matches_from_safe(args)
.unwrap();
let parsed =
parse_mwixnet_args(matches.subcommand_matches("mwixnet").unwrap()).unwrap();
assert!(matches!(parsed.output, command::MwixnetOutput::Max));
assert_eq!(parsed.params.server_keys.len(), key_count);
}
}
#[test]
fn parses_mwixnet_output_selection() {
assert!(matches!(
parse_mwixnet_output("max").unwrap(),
command::MwixnetOutput::Max
));
assert!(matches!(
parse_mwixnet_output("10").unwrap(),
command::MwixnetOutput::MinimumAmount(10_000_000_000)
));
assert!(matches!(
parse_mwixnet_output(&format!("08{}", "00".repeat(32))).unwrap(),
command::MwixnetOutput::Commitment(_)
));
}
}
+127
View File
@@ -235,6 +235,12 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::
"10",
];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
let args = vec!["grin-wallet", "-p", "unused", "unpack", "-i", &file_name];
assert_eq!(
execute_command(&app, test_dir, "wallet1", &client1, args)?,
"unpack"
);
let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
@@ -810,6 +816,127 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::
let (_, wallet_info) = api2.retrieve_summary_info(mask2, true, 1)?;
assert!(wallet_info.amount_currently_spendable < old_balance);
// Issue encrypted invoice using custom address index, wallet2.
let arg_vec = vec![
"grin-wallet",
"-a",
"default",
"-p",
"password2",
"invoice",
"--address_index",
"3",
"-d",
"tgrin1fu5z9y4p8atxr83l6nfj0j2m067r9gjfkd6mzeuuz0v6sqftua2quv0ll3",
"1",
];
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
let file_name = format!(
"{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b08.I1.slatepack",
test_dir
);
// Pay the invoice tx, wallet 1
let arg_vec = vec![
"grin-wallet",
"-a",
"default",
"-p",
"password1",
"pay",
"-i",
&file_name,
];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
let file_name = format!(
"{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b08.I2.slatepack",
test_dir
);
// Finalize the invoice, wallet 2
let arg_vec = vec![
"grin-wallet",
"-a",
"default",
"-p",
"password2",
"finalize",
"-i",
&file_name,
];
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
// Send encrypted to wallet 2 using custom address index
let arg_vec = vec![
"grin-wallet",
"-p",
"password1",
"-a",
"mining",
"send",
"-d",
"tgrin1ak8aaxpjg6ct5uje4lgzvjp65l0nrmgxndp5xjy74sumzp7wasysje3kmf",
"--address_index",
"5",
"1",
];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
// Receive, wallet 2
let file_name = format!(
"{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b09.S1.slatepack",
test_dir
);
for (wallet, password, account, client) in [
("wallet1", "password1", "mining", &client1),
("wallet2", "password2", "account_1", &client2),
] {
let args = vec![
"grin-wallet",
"-p",
password,
"-a",
account,
"unpack",
"-i",
&file_name,
];
assert_eq!(
execute_command(&app, test_dir, wallet, client, args)?,
"unpack"
);
}
let arg_vec = vec![
"grin-wallet",
"-p",
"password2",
"-a",
"account_1",
"receive",
"-i",
&file_name,
];
execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?;
let file_name = format!(
"{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b09.S2.slatepack",
test_dir
);
// Finalize the send, wallet 1
let arg_vec = vec![
"grin-wallet",
"-a",
"mining",
"-p",
"password1",
"finalize",
"-i",
&file_name,
];
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
// let logging finish
thread::sleep(Duration::from_millis(200));
clean_output_dir(test_dir);