// 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. //! Types and traits that should be provided by a wallet //! implementation use crate::config::{TorConfig, WalletConfig}; use crate::error::Error; use crate::grin_core::core::hash::Hash; use crate::grin_core::core::FeeFields; use crate::grin_core::core::{Output, Transaction, TxKernel}; use crate::grin_core::libtx::{aggsig, secp_ser}; use crate::grin_core::{global, ser}; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::logger::LoggingConfig; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::{pedersen, Secp256k1}; use crate::grin_util::{ToHex, ZeroingString}; use crate::slate_versions::ser as dalek_ser; use crate::{InitTxArgs, SlateState, WalletBackend}; use chrono::prelude::*; use ed25519_dalek::Signature as DalekSignature; use ed25519_dalek::VerifyingKey as DalekPublicKey; use rand::rngs::mock::StepRng; use rand::thread_rng; use serde; use serde_json; use std::collections::HashMap; use std::time::Duration; use std::{cmp, fmt}; use uuid::Uuid; /// Combined trait to allow dynamic wallet dispatch pub trait WalletInst<'a, L, C, K>: Send + Sync where L: WalletLCProvider<'a, C, K> + Send + Sync, C: NodeClient + 'a, K: Keychain + 'a, { /// Return the stored instance fn lc_provider(&mut self) -> Result<&mut dyn WalletLCProvider<'a, C, K>, Error>; } /// Trait for a provider of wallet lifecycle methods pub trait WalletLCProvider<'a, C, K>: Send + Sync where C: NodeClient + 'a, K: Keychain + 'a, { /// Sets the top level system wallet directory /// default is assumed to be ~/.grin/main/wallet_data (or testnet equivalent) fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error>; /// Sets the top level system wallet directory /// default is assumed to be ~/.grin/main/wallet_data (or testnet equivalent) fn get_top_level_directory(&self) -> Result; /// Output a grin-wallet.toml file into the current top-level system wallet directory fn create_config( &self, chain_type: &global::ChainTypes, file_name: &str, wallet_config: Option, logging_config: Option, tor_config: Option, ) -> Result<(), Error>; /// fn create_wallet( &mut self, name: Option<&str>, mnemonic: Option, entropy_size: usize, password: ZeroingString, test_mode: bool, ) -> Result<(), Error>; /// fn open_wallet( &mut self, name: Option<&str>, password: ZeroingString, create_mask: bool, use_test_rng: bool, ) -> Result, Error>; /// fn close_wallet(&mut self, name: Option<&str>) -> Result<(), Error>; /// whether a wallet exists at the given directory fn wallet_exists(&self, name: Option<&str>) -> Result; /// return mnemonic of given wallet fn get_mnemonic( &self, name: Option<&str>, password: ZeroingString, ) -> Result; /// Check whether a provided mnemonic string is valid fn validate_mnemonic(&self, mnemonic: ZeroingString) -> Result<(), Error>; /// Recover a seed from phrase, without destroying existing data /// should back up seed fn recover_from_mnemonic( &self, mnemonic: ZeroingString, password: ZeroingString, ) -> Result<(), Error>; /// changes password fn change_password( &self, name: Option<&str>, old: ZeroingString, new: ZeroingString, ) -> Result<(), Error>; /// deletes wallet fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error>; /// return wallet instance fn wallet_inst(&mut self) -> Result<&mut WalletBackend, Error>; } /// Encapsulate all wallet-node communication functions. No functions within libwallet /// should care about communication details pub trait NodeClient: Send + Sync + Clone { /// Return the URL of the check node fn node_url(&self) -> &str; /// Set the node URL fn set_node_url(&mut self, node_url: &str); /// Return the node api secret fn node_api_secret(&self) -> Option; /// Change the API secret fn set_node_api_secret(&mut self, node_api_secret: Option); /// Posts a transaction to a grin node fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), Error>; /// Returns the api version string and block header version as reported /// by the node. Result can be cached for later use fn get_version_info(&mut self) -> Option; /// retrieves the current tip (height, hash) from the specified grin node fn get_chain_tip(&self) -> Result<(u64, String), Error>; /// Get a kernel and the height of the block it's included in. Returns /// (tx_kernel, height, mmr_index) fn get_kernel( &mut self, excess: &pedersen::Commitment, min_height: Option, max_height: Option, ) -> Result, Error>; /// retrieve a list of outputs from the specified grin node /// need "by_height" and "by_id" variants fn get_outputs_from_node( &self, wallet_outputs: Vec, ) -> Result, Error>; /// Get a list of outputs from the node by traversing the UTXO /// set in PMMR index order. /// Returns /// (last available output index, last insertion index retrieved, /// outputs(commit, proof, is_coinbase, height, mmr_index)) fn get_outputs_by_pmmr_index( &self, start_height: u64, end_height: Option, max_outputs: u64, ) -> Result< ( u64, u64, Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, ), Error, >; /// Return the pmmr indices representing the outputs between a given /// set of block heights /// (start pmmr index, end pmmr index) fn height_range_to_pmmr_indices( &self, start_height: u64, end_height: Option, ) -> Result<(u64, u64), Error>; } /// Node version info #[derive(Serialize, Deserialize, Debug, Clone)] pub struct NodeVersionInfo { /// Semver version string pub node_version: String, /// block header verson pub block_header_version: u16, /// Whether this version info was successfully verified from a node pub verified: Option, } /// Information about an output that's being tracked by the wallet. Must be /// enough to reconstruct the commitment associated with the ouput when the /// root private key is known. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct OutputData { /// Root key_id that the key for this output is derived from pub root_key_id: Identifier, /// Derived key for this output pub key_id: Identifier, /// How many derivations down from the root key pub n_child: u32, /// The actual commit, optionally stored pub commit: Option, /// PMMR Index, used on restore in case of duplicate wallets using the same /// key_id (2 wallets using same seed, for instance #[serde(with = "secp_ser::opt_string_or_u64")] pub mmr_index: Option, /// Value of the output, necessary to rebuild the commitment #[serde(with = "secp_ser::string_or_u64")] pub value: u64, /// Current status of the output pub status: OutputStatus, /// Height of the output #[serde(with = "secp_ser::string_or_u64")] pub height: u64, /// Height we are locked until #[serde(with = "secp_ser::string_or_u64")] pub lock_height: u64, /// Is this a coinbase output? Is it subject to coinbase locktime? pub is_coinbase: bool, /// Optional corresponding internal entry in tx entry log pub tx_log_entry: Option, } impl ser::Writeable for OutputData { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for OutputData { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } impl OutputData { /// Lock a given output to avoid conflicting use pub fn lock(&mut self) { self.status = OutputStatus::Locked; } /// How many confirmations has this output received? /// If height == 0 then we are either Unconfirmed or the output was /// cut-through /// so we do not actually know how many confirmations this output had (and /// never will). pub fn num_confirmations(&self, current_height: u64) -> u64 { if self.height > current_height { return 0; } if self.status == OutputStatus::Unconfirmed { 0 } else { // if an output has height n and we are at block n // then we have a single confirmation (the block it originated in) 1 + (current_height - self.height) } } /// Check if output is eligible to spend based on state and height and /// confirmations pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool { if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) || self.status == OutputStatus::Unconfirmed && self.is_coinbase || self.lock_height > current_height { false } else { (self.status == OutputStatus::Unspent && self.num_confirmations(current_height) >= minimum_confirmations) || self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 } } /// Marks this output as unspent if it was previously unconfirmed pub fn mark_unspent(&mut self) { match self.status { OutputStatus::Unconfirmed | OutputStatus::Reverted => { self.status = OutputStatus::Unspent } _ => {} } } /// Mark an output as spent pub fn mark_spent(&mut self) { match self.status { OutputStatus::Unspent | OutputStatus::Locked => self.status = OutputStatus::Spent, _ => (), } } /// Mark an output as reverted pub fn mark_reverted(&mut self) { match self.status { OutputStatus::Unspent => self.status = OutputStatus::Reverted, _ => (), } } } /// Status of an output that's being tracked by the wallet. Can either be /// unconfirmed, spent, unspent, or locked (when it's been used to generate /// a transaction but we don't have confirmation that the transaction was /// broadcasted or mined). #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub enum OutputStatus { /// Unconfirmed Unconfirmed, /// Unspent Unspent, /// Locked Locked, /// Spent Spent, /// Reverted Reverted, } impl fmt::Display for OutputStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), OutputStatus::Unspent => write!(f, "Unspent"), OutputStatus::Locked => write!(f, "Locked"), OutputStatus::Spent => write!(f, "Spent"), OutputStatus::Reverted => write!(f, "Reverted"), } } } /// Maximum private transaction context size. /// Verified against `max_tx_weight()` below. const MAX_CONTEXT_SIZE: usize = 4 * 1024 * 1024; #[derive(Serialize, Deserialize, Clone, Debug)] /// Holds the context for a single aggsig transaction pub struct Context { /// Parent key id pub parent_key_id: Identifier, /// Secret key (of which public is shared) pub sec_key: SecretKey, /// Secret nonce (of which public is shared) /// (basically a SecretKey) pub sec_nonce: SecretKey, /// only used if self-sending an invoice pub initial_sec_key: SecretKey, /// as above pub initial_sec_nonce: SecretKey, /// store my outputs + amounts between invocations /// Id, mmr_index (if known), amount pub output_ids: Vec<(Identifier, Option, u64)>, /// store my inputs /// Id, mmr_index (if known), amount pub input_ids: Vec<(Identifier, Option, u64)>, /// store amount, so we can remove from slate if not /// needed by the other party pub amount: u64, /// store the calculated fee pub fee: Option, /// Payment proof sender address derivation path, if needed pub payment_proof_derivation_index: Option, /// If late-locking, store my tranasction creation prefs /// for later pub late_lock_args: Option, /// for invoice I2 Only, store the tx excess so we can /// remove it from the slate on return pub calculated_excess: Option, } impl Context { /// Create a new context with defaults pub fn new( secp: &Secp256k1, parent_key_id: &Identifier, use_test_rng: bool, is_initiator: bool, ) -> Self { let sec_key = match use_test_rng { false => SecretKey::new(secp, &mut thread_rng()), true => { // allow for consistent test results let mut test_rng = if is_initiator { StepRng::new(1_234_567_890_u64, 1) } else { StepRng::new(1_234_567_891_u64, 1) }; SecretKey::new(secp, &mut test_rng) } }; Self::with_excess(secp, sec_key, parent_key_id, use_test_rng) } /// Create a new context with a specific excess pub fn with_excess( secp: &Secp256k1, sec_key: SecretKey, parent_key_id: &Identifier, use_test_rng: bool, ) -> Self { let sec_nonce = match use_test_rng { false => aggsig::create_secnonce(secp).unwrap(), true => SecretKey::from_slice(secp, &[1; 32]).unwrap(), }; Self { parent_key_id: parent_key_id.clone(), sec_key: sec_key.clone(), sec_nonce: sec_nonce.clone(), initial_sec_key: sec_key, initial_sec_nonce: sec_nonce, input_ids: vec![], output_ids: vec![], amount: 0, fee: None, payment_proof_derivation_index: None, late_lock_args: None, calculated_excess: None, } } } impl Context { /// Tracks an output contributing to my excess value (if it needs to /// be kept between invocations pub fn add_output(&mut self, output_id: &Identifier, mmr_index: &Option, amount: u64) { self.output_ids .push((output_id.clone(), *mmr_index, amount)); } /// Returns all stored outputs pub fn get_outputs(&self) -> Vec<(Identifier, Option, u64)> { self.output_ids.clone() } /// Tracks IDs of my inputs into the transaction /// be kept between invocations pub fn add_input(&mut self, input_id: &Identifier, mmr_index: &Option, amount: u64) { self.input_ids.push((input_id.clone(), *mmr_index, amount)); } /// Returns all stored input identifiers pub fn get_inputs(&self) -> Vec<(Identifier, Option, u64)> { self.input_ids.clone() } /// Returns private key, private nonce pub fn get_private_keys(&self) -> (SecretKey, SecretKey) { (self.sec_key.clone(), self.sec_nonce.clone()) } /// Returns public key, public nonce pub fn get_public_keys(&self, secp: &Secp256k1) -> (PublicKey, PublicKey) { ( PublicKey::from_secret_key(secp, &self.sec_key).unwrap(), PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(), ) } } impl ser::Writeable for Context { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { let data = serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?; if data.len() > MAX_CONTEXT_SIZE { return Err(ser::Error::TooLargeReadErr); } writer.write_bytes(&data) } } impl ser::Readable for Context { fn read(reader: &mut R) -> Result { let data = read_bytes_len_prefix(reader)?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } /// Block Identifier #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct BlockIdentifier(pub Hash); impl BlockIdentifier { /// return hash pub fn hash(&self) -> Hash { self.0 } /// convert to hex string pub fn from_hex(hex: &str) -> Result { let hash = Hash::from_hex(hex).map_err(|e| Error::GenericError(format!("Invalid hex: {}", e)))?; Ok(BlockIdentifier(hash)) } } impl serde::ser::Serialize for BlockIdentifier { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(&self.0.to_hex()) } } impl<'de> serde::de::Deserialize<'de> for BlockIdentifier { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { deserializer.deserialize_str(BlockIdentifierVisitor) } } struct BlockIdentifierVisitor; impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor { type Value = BlockIdentifier; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a block hash") } fn visit_str(self, s: &str) -> Result where E: serde::de::Error, { let block_hash = Hash::from_hex(s).unwrap(); Ok(BlockIdentifier(block_hash)) } } /// a contained wallet info struct, so automated tests can parse wallet info /// can add more fields here over time as needed #[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Clone)] pub struct WalletInfo { /// height from which info was taken #[serde(with = "secp_ser::string_or_u64")] pub last_confirmed_height: u64, /// Minimum number of confirmations for an output to be treated as "spendable". #[serde(with = "secp_ser::string_or_u64")] pub minimum_confirmations: u64, /// total amount in the wallet #[serde(with = "secp_ser::string_or_u64")] pub total: u64, /// amount awaiting finalization #[serde(with = "secp_ser::string_or_u64")] pub amount_awaiting_finalization: u64, /// amount awaiting confirmation #[serde(with = "secp_ser::string_or_u64")] pub amount_awaiting_confirmation: u64, /// coinbases waiting for lock height #[serde(with = "secp_ser::string_or_u64")] pub amount_immature: u64, /// amount currently spendable #[serde(with = "secp_ser::string_or_u64")] pub amount_currently_spendable: u64, /// amount locked via previous transactions #[serde(with = "secp_ser::string_or_u64")] pub amount_locked: u64, /// amount previously confirmed, now reverted #[serde(with = "secp_ser::string_or_u64")] pub amount_reverted: u64, } /// Types of transactions that can be contained within a TXLog entry #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub enum TxLogEntryType { /// A coinbase transaction becomes confirmed ConfirmedCoinbase, /// Outputs created when a transaction is received TxReceived, /// Inputs locked + change outputs when a transaction is created TxSent, /// Received transaction that was rolled back by user TxReceivedCancelled, /// Sent transaction that was rolled back by user TxSentCancelled, /// Received transaction that was reverted on-chain TxReverted, } impl fmt::Display for TxLogEntryType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"), TxLogEntryType::TxReceived => write!(f, "Received Tx"), TxLogEntryType::TxSent => write!(f, "Sent Tx"), TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"), TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"), TxLogEntryType::TxReverted => write!(f, "Received Tx\n- Reverted"), } } } /// Optional transaction information, recorded when an event happens /// to add or remove funds from a wallet. One Transaction log entry /// maps to one or many outputs #[derive(Serialize, Deserialize, Debug, Clone)] pub struct TxLogEntry { /// BIP32 account path used for creating this tx pub parent_key_id: Identifier, /// Local id for this transaction (distinct from a slate transaction id) pub id: u32, /// Slate transaction this entry is associated with, if any pub tx_slate_id: Option, /// Transaction slate state pub tx_slate_state: Option, /// Transaction type (as above) pub tx_type: TxLogEntryType, /// Time this tx entry was created /// #[serde(with = "tx_date_format")] pub creation_ts: DateTime, /// Time this tx was confirmed (by this wallet) /// #[serde(default, with = "opt_tx_date_format")] pub confirmation_ts: Option>, /// Whether the inputs+outputs involved in this transaction have been /// confirmed (In all cases either all outputs involved in a tx should be /// confirmed, or none should be; otherwise there's a deeper problem) pub confirmed: bool, /// number of inputs involved in TX pub num_inputs: usize, /// number of outputs involved in TX pub num_outputs: usize, /// Amount credited via this transaction #[serde(with = "secp_ser::string_or_u64")] pub amount_credited: u64, /// Amount debited via this transaction #[serde(with = "secp_ser::string_or_u64")] pub amount_debited: u64, /// Fee pub fee: Option, /// Cutoff block height #[serde(with = "secp_ser::opt_string_or_u64")] #[serde(default)] pub ttl_cutoff_height: Option, /// Location of the store transaction, (reference or resending) pub stored_tx: Option, /// Associated kernel excess, for later lookup if necessary #[serde(with = "secp_ser::option_commitment_serde")] #[serde(default)] pub kernel_excess: Option, /// Height reported when transaction was created, if lookup /// of kernel is necessary #[serde(default)] pub kernel_lookup_min_height: Option, /// Additional info needed to stored payment proof #[serde(default)] pub payment_proof: Option, /// Track the time it took for a transaction to get reverted #[serde(with = "option_duration_as_secs", default)] pub reverted_after: Option, } impl ser::Writeable for TxLogEntry { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for TxLogEntry { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } impl TxLogEntry { /// Return a new blank with TS initialised with next entry pub fn new(parent_key_id: Identifier, t: TxLogEntryType, id: u32) -> Self { TxLogEntry { parent_key_id, tx_type: t, id, tx_slate_id: None, tx_slate_state: None, creation_ts: Utc::now(), confirmation_ts: None, confirmed: false, amount_credited: 0, amount_debited: 0, num_inputs: 0, num_outputs: 0, fee: None, ttl_cutoff_height: None, stored_tx: None, kernel_excess: None, kernel_lookup_min_height: None, payment_proof: None, reverted_after: None, } } /// Given a vec of TX log entries, return credited + debited sums pub fn sum_confirmed(txs: &[TxLogEntry]) -> (u64, u64) { txs.iter().fold((0, 0), |acc, tx| match tx.confirmed { true => (acc.0 + tx.amount_credited, acc.1 + tx.amount_debited), false => acc, }) } /// Update confirmation TS with now pub fn update_confirmation_ts(&mut self) { self.confirmation_ts = Some(Utc::now()); } } /// Payment proof information. Differs from what is sent via /// the slate #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StoredProofInfo { /// receiver address #[serde(with = "dalek_ser::dalek_pubkey_serde")] pub receiver_address: DalekPublicKey, #[serde(with = "dalek_ser::option_dalek_sig_serde")] /// receiver signature pub receiver_signature: Option, /// sender address derivation path index pub sender_address_path: u32, /// sender address #[serde(with = "dalek_ser::dalek_pubkey_serde")] pub sender_address: DalekPublicKey, /// sender signature #[serde(with = "dalek_ser::option_dalek_sig_serde")] pub sender_signature: Option, } impl ser::Writeable for StoredProofInfo { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for StoredProofInfo { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } /// Map of named accounts to BIP32 paths #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AcctPathMapping { /// label used by user pub label: String, /// Corresponding parent BIP32 derivation path pub path: Identifier, } impl ser::Writeable for AcctPathMapping { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for AcctPathMapping { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } /// Dummy wrapper for the hex-encoded serialized transaction. #[derive(Serialize, Deserialize)] pub struct TxWrapper { /// hex representation of transaction pub tx_hex: String, } /// Store details of the last scanned block #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ScannedBlockInfo { /// Node chain height (corresponding to the last PMMR index scanned) pub height: u64, /// Hash of tip pub hash: String, /// Starting PMMR Index pub start_pmmr_index: u64, /// Last PMMR Index pub last_pmmr_index: u64, } impl ser::Writeable for ScannedBlockInfo { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for ScannedBlockInfo { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } /// Wrapper for reward output and kernel used when building a coinbase for a mining node. /// Note: Not serializable, must be converted to necesssary "versioned" representation /// before serializing to json to ensure compatibility with mining node. #[derive(Debug, Clone)] pub struct CbData { /// Output pub output: Output, /// Kernel pub kernel: TxKernel, /// Key Id pub key_id: Option, } /// Enum to determine what amount of scanning is required for a new wallet #[derive(Debug, Clone, Serialize, Deserialize)] pub enum WalletInitStatus { /// Wallet is newly created and needs scanning InitNeedsScanning, /// Wallet is new but doesn't need scanning InitNoScanning, /// Wallet scan checks have been completed InitComplete, } impl ser::Writeable for WalletInitStatus { fn write(&self, writer: &mut W) -> Result<(), ser::Error> { writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) } } impl ser::Readable for WalletInitStatus { fn read(reader: &mut R) -> Result { let data = reader.read_bytes_len_prefix()?; serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) } } /// Utility struct for return values from below #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ViewWallet { /// Rewind Hash used to retrieve the outputs pub rewind_hash: String, /// All outputs information that belongs to the rewind hash pub output_result: Vec, /// total balance pub total_balance: u64, /// last pmmr index pub last_pmmr_index: u64, } /// Utility struct for return values from below #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ViewWalletOutputResult { /// pub commit: String, /// pub value: u64, /// pub height: u64, /// pub mmr_index: u64, /// pub is_coinbase: bool, /// pub lock_height: u64, } impl ViewWalletOutputResult { pub fn num_confirmations(&self, tip_height: u64) -> u64 { if self.height > tip_height { 0 } else { 1 + (tip_height - self.height) } } } fn read_bytes_len_prefix(reader: &mut R) -> Result, ser::Error> { let len = reader.read_u64()?; if len > MAX_CONTEXT_SIZE as u64 { return Err(ser::Error::TooLargeReadErr); } let mut len = len as usize; match reader.read_limit() { None => reader.read_fixed_bytes(len), Some(limit) => { if limit == 0 { return reader.read_fixed_bytes(len); } let mut data = vec![]; loop { if len == 0 { break; } let read_size = cmp::min(len, limit); let read_data = reader.read_fixed_bytes(read_size)?; data.extend_from_slice(&read_data); len -= read_size; } Ok(data) } } } /// Serializes an Option to and from a string pub mod option_duration_as_secs { use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; use std::time::Duration; /// pub fn serialize(dur: &Option, serializer: S) -> Result where S: Serializer, { match dur { Some(dur) => serializer.serialize_str(&format!("{}", dur.as_secs())), None => serializer.serialize_none(), } } /// pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { match Option::::deserialize(deserializer)? { Some(s) => { let secs = s .parse::() .map_err(|err| Error::custom(err.to_string()))?; Ok(Some(Duration::from_secs(secs))) } None => Ok(None), } } } #[cfg(test)] mod tests { use super::*; use grin_core::ser::{DeserializationMode, ProtocolVersion, Readable, Reader, StreamingReader}; use grin_keychain::{ExtKeychain, ExtKeychainPath}; use serde_json::Value; #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] struct TestSer { #[serde(with = "option_duration_as_secs", default)] dur: Option, } #[test] fn duration_serde() { let some = TestSer { dur: Some(Duration::from_secs(100)), }; let val = serde_json::to_value(some.clone()).unwrap(); if let Value::Object(o) = &val { if let Value::String(s) = o.get("dur").unwrap() { assert_eq!(s, "100"); } else { panic!("Invalid type"); } } else { panic!("Invalid type") } assert_eq!(some, serde_json::from_value(val).unwrap()); let none = TestSer { dur: None }; let val = serde_json::to_value(none.clone()).unwrap(); if let Value::Object(o) = &val { if let Value::Null = o.get("dur").unwrap() { // ok } else { panic!("Invalid type"); } } else { panic!("Invalid type") } assert_eq!(none, serde_json::from_value(val).unwrap()); let none2 = serde_json::from_str::("{}").unwrap(); assert_eq!(none, none2); } #[test] fn big_context_read() { let protocol_ver = ProtocolVersion(3); let create_context = || { let parent = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); let mut context = Context::new(sender_keychain.secp(), &parent, false, true); for i in 0..3000 { let key_id = ExtKeychain::derive_key_id(1, 1, i, 0, 0); context.add_output(&key_id, &None, i as u64); } context }; { let context = create_context(); let ser_value = ser::ser_vec(&context, protocol_ver).unwrap(); let mut value = ser_value.as_slice(); assert!(value.len() > 100_000); let context = ser::deserialize::( &mut value, protocol_ver, DeserializationMode::Full, ); assert!(context.is_ok()); } // StreamingReader has no per-read limit and reads the complete Context. let context = create_context(); let ser_value = ser::ser_vec(&context, protocol_ver).unwrap(); let mut value = ser_value.as_slice(); let mut streaming_reader = StreamingReader::new(&mut value, protocol_ver); assert!(streaming_reader.read_limit().is_none()); let res = Context::read(&mut streaming_reader); assert_eq!(res.unwrap().get_outputs().len(), 3000); } #[test] fn context_size_limit() { let protocol_ver = ProtocolVersion(3); let oversized_len = ((MAX_CONTEXT_SIZE + 1) as u64).to_be_bytes(); let mut oversized_prefix = oversized_len.as_slice(); let read = ser::deserialize::( &mut oversized_prefix, protocol_ver, DeserializationMode::Full, ); assert_eq!(read.unwrap_err(), ser::Error::TooLargeReadErr); let parent = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); let mut context = Context::new(sender_keychain.secp(), &parent, false, true); context.late_lock_args = Some(InitTxArgs { src_acct_name: Some("x".repeat(MAX_CONTEXT_SIZE)), ..Default::default() }); assert_eq!( ser::ser_vec(&context, protocol_ver).unwrap_err(), ser::Error::TooLargeReadErr ); } #[test] fn max_weight_context_size() { global::set_local_chain_type(global::ChainTypes::Mainnet); let max_tx_weight = global::max_tx_weight(); let max_inputs = (1..) .take_while(|inputs| Transaction::weight_by_iok(*inputs, 1, 1) <= max_tx_weight) .last() .unwrap(); let parent = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); let mut context = Context::new(sender_keychain.secp(), &parent, true, true); for i in 0..max_inputs { let key_id = ExtKeychain::derive_key_id(1, 1, i as u32, 0, 0); context.add_input(&key_id, &Some(u64::MAX), u64::MAX); } let size = serde_json::to_vec(&context).unwrap().len(); println!("max-weight Context: {size} bytes ({max_inputs} inputs)"); assert!( size <= MAX_CONTEXT_SIZE, "max-weight Context is {size} bytes, limit is {MAX_CONTEXT_SIZE}" ); } }