/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ collections::{BTreeMap, HashMap}, str::FromStr, }; use lazy_static::lazy_static; use rand::rngs::OsRng; use darkfi::{ tx::Transaction, util::encoding::base64, zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof}, zkas::ZkBinary, Error, Result, }; use darkfi_money_contract::{ client::{ compute_remainder_blind, fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS}, MoneyNote, OwnCoin, }, model::{ Coin, Input, MoneyAuthTokenFreezeParamsV1, MoneyBurnParamsV1, MoneyFeeParamsV1, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenMintParamsV1, MoneyTransferParamsV1, Nullifier, Output, TokenId, DARK_TOKEN_ID, }, MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1, }; use darkfi_sdk::{ blockchain::compute_fee, bridgetree::Position, crypto::{ keypair::{Address, Keypair, PublicKey, SecretKey, StandardAddress}, note::AeadEncryptedNote, pasta_prelude::PrimeField, BaseBlind, FuncId, MerkleNode, MerkleTree, ScalarBlind, MONEY_CONTRACT_ID, }, dark_tree::DarkLeaf, pasta::pallas, ContractCall, }; use darkfi_serial::{deserialize, deserialize_async, serialize, serialize_async, AsyncEncodable}; use crate::{ cache::CacheSmt, cli_util::kaching, convert_named_params, error::{WalletDbError, WalletDbResult}, params, rpc::ScanCache, walletdb::Value, Drk, }; // Money Merkle tree Sled key pub const SLED_MERKLE_TREES_MONEY: &[u8] = b"_money_tree"; // Wallet SQL table constant names. These have to represent the `money.sql` // SQL schema. Table names are prefixed with the contract ID to avoid collisions. lazy_static! { pub static ref MONEY_KEYS_TABLE: String = format!("{}_money_keys", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_COINS_TABLE: String = format!("{}_money_coins", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_TOKENS_TABLE: String = format!("{}_money_tokens", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_ALIASES_TABLE: String = format!("{}_money_aliases", MONEY_CONTRACT_ID.to_string()); } // MONEY_KEYS_TABLE pub const MONEY_KEYS_COL_KEY_ID: &str = "key_id"; pub const MONEY_KEYS_COL_IS_DEFAULT: &str = "is_default"; pub const MONEY_KEYS_COL_PUBLIC: &str = "public"; pub const MONEY_KEYS_COL_SECRET: &str = "secret"; // MONEY_COINS_TABLE pub const MONEY_COINS_COL_COIN: &str = "coin"; pub const MONEY_COINS_COL_VALUE: &str = "value"; pub const MONEY_COINS_COL_TOKEN_ID: &str = "token_id"; pub const MONEY_COINS_COL_SPEND_HOOK: &str = "spend_hook"; pub const MONEY_COINS_COL_USER_DATA: &str = "user_data"; pub const MONEY_COINS_COL_COIN_BLIND: &str = "coin_blind"; pub const MONEY_COINS_COL_VALUE_BLIND: &str = "value_blind"; pub const MONEY_COINS_COL_TOKEN_BLIND: &str = "token_blind"; pub const MONEY_COINS_COL_SECRET: &str = "secret"; pub const MONEY_COINS_COL_LEAF_POSITION: &str = "leaf_position"; pub const MONEY_COINS_COL_MEMO: &str = "memo"; pub const MONEY_COINS_COL_CREATION_HEIGHT: &str = "creation_height"; pub const MONEY_COINS_COL_IS_SPENT: &str = "is_spent"; pub const MONEY_COINS_COL_SPENT_HEIGHT: &str = "spent_height"; pub const MONEY_COINS_COL_SPENT_TX_HASH: &str = "spent_tx_hash"; // MONEY_TOKENS_TABLE pub const MONEY_TOKENS_COL_TOKEN_ID: &str = "token_id"; pub const MONEY_TOKENS_COL_MINT_AUTHORITY: &str = "mint_authority"; pub const MONEY_TOKENS_COL_TOKEN_BLIND: &str = "token_blind"; pub const MONEY_TOKENS_COL_IS_FROZEN: &str = "is_frozen"; pub const MONEY_TOKENS_COL_FREEZE_HEIGHT: &str = "freeze_height"; // MONEY_ALIASES_TABLE pub const MONEY_ALIASES_COL_ALIAS: &str = "alias"; pub const MONEY_ALIASES_COL_TOKEN_ID: &str = "token_id"; pub const BALANCE_BASE10_DECIMALS: usize = 8; impl Drk { /// Initialize wallet with tables for the Money contract. pub async fn initialize_money(&self, output: &mut Vec) -> WalletDbResult<()> { // Initialize Money wallet schema let wallet_schema = include_str!("../money.sql"); self.wallet.exec_batch_sql(wallet_schema).await?; // Insert DRK alias self.add_alias("DRK".to_string(), *DARK_TOKEN_ID, output).await?; Ok(()) } /// Generate a new keypair and place it into the wallet. pub async fn money_keygen(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Generating a new keypair")); // TODO: We might want to have hierarchical deterministic key derivation. let keypair = Keypair::random(&mut OsRng); let is_default = 0; let query = format!( "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET ); self.wallet .exec_sql( &query, params![ is_default, serialize_async(&keypair.public).await, serialize_async(&keypair.secret).await ], ) .await?; output.push(String::from("New address:")); let address: Address = StandardAddress::from_public(self.network, keypair.public).into(); output.push(format!("{address}")); Ok(()) } /// Fetch default secret key from the wallet. pub async fn default_secret(&self) -> Result { let row = match self .wallet .query_single( &MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_SECRET], convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, ) .await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[default_secret] Default secret key retrieval failed: {e}" ))) } }; let Value::Blob(ref key_bytes) = row[0] else { return Err(Error::ParseFailed("[default_secret] Key bytes parsing failed")) }; let secret_key: SecretKey = deserialize_async(key_bytes).await?; Ok(secret_key) } /// Fetch default pubkey from the wallet. pub async fn default_address(&self) -> Result { let row = match self .wallet .query_single( &MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_PUBLIC], convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, ) .await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[default_address] Default address retrieval failed: {e}" ))) } }; let Value::Blob(ref key_bytes) = row[0] else { return Err(Error::ParseFailed("[default_address] Key bytes parsing failed")) }; let public_key: PublicKey = deserialize_async(key_bytes).await?; Ok(public_key) } /// Set provided index address as default in the wallet. pub async fn set_default_address(&self, idx: u16) -> WalletDbResult<()> { // First we update previous default record let is_default = 0; let query = format!("UPDATE {} SET {} = ?1", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT,); self.wallet.exec_sql(&query, params![is_default]).await?; // and then we set the new one let is_default = 1; let query = format!( "UPDATE {} SET {} = ?1 WHERE {} = ?2", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_KEY_ID, ); self.wallet.exec_sql(&query, params![is_default, idx]).await } /// Fetch all pukeys from the wallet. pub async fn addresses(&self) -> Result> { let rows = match self.wallet.query_multiple(&MONEY_KEYS_TABLE, &[], vec![]).await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[addresses] Addresses retrieval failed: {e}" ))) } }; let mut vec = Vec::with_capacity(rows.len()); for row in rows { let Value::Integer(key_id) = row[0] else { return Err(Error::ParseFailed("[addresses] Key ID parsing failed")) }; let Ok(key_id) = u64::try_from(key_id) else { return Err(Error::ParseFailed("[addresses] Key ID parsing failed")) }; let Value::Integer(is_default) = row[1] else { return Err(Error::ParseFailed("[addresses] Is default parsing failed")) }; let Ok(is_default) = u64::try_from(is_default) else { return Err(Error::ParseFailed("[addresses] Is default parsing failed")) }; let Value::Blob(ref key_bytes) = row[2] else { return Err(Error::ParseFailed("[addresses] Public key bytes parsing failed")) }; let public_key: PublicKey = deserialize_async(key_bytes).await?; let Value::Blob(ref key_bytes) = row[3] else { return Err(Error::ParseFailed("[addresses] Secret key bytes parsing failed")) }; let secret_key: SecretKey = deserialize_async(key_bytes).await?; vec.push((key_id, public_key, secret_key, is_default)); } Ok(vec) } /// Fetch provided index address from the wallet and generate its /// mining configuration. pub async fn mining_config( &self, idx: u16, spend_hook: Option, user_data: Option, output: &mut Vec, ) -> Result<()> { let row = match self .wallet .query_single( &MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_PUBLIC], convert_named_params! {(MONEY_KEYS_COL_KEY_ID, idx)}, ) .await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[mining_address] Address retrieval failed: {e}" ))) } }; let Value::Blob(ref key_bytes) = row[0] else { return Err(Error::ParseFailed("[mining_address] Key bytes parsing failed")) }; let public_key: PublicKey = deserialize_async(key_bytes).await?; let address: Address = StandardAddress::from_public(self.network, public_key).into(); let recipient = address.to_string(); let spend_hook = spend_hook.as_ref().map(|spend_hook| spend_hook.to_string()); let user_data = user_data.as_ref().map(|user_data| bs58::encode(user_data.to_repr()).into_string()); output.push(String::from("DarkFi mining configuration address:")); output.push(base64::encode(&serialize(&(recipient, spend_hook, user_data))).to_string()); Ok(()) } /// Fetch all secret keys from the wallet. pub async fn get_money_secrets(&self) -> Result> { let rows = match self .wallet .query_multiple(&MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_SECRET], vec![]) .await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_money_secrets] Secret keys retrieval failed: {e}" ))) } }; let mut secrets = Vec::with_capacity(rows.len()); // Let's scan through the rows and see if we got anything. for row in rows { let Value::Blob(ref key_bytes) = row[0] else { return Err(Error::ParseFailed( "[get_money_secrets] Secret key bytes parsing failed", )) }; let secret_key: SecretKey = deserialize_async(key_bytes).await?; secrets.push(secret_key); } Ok(secrets) } /// Import given secret keys into the wallet. /// If the key already exists, it will be skipped. /// Returns the respective PublicKey objects for the imported keys. pub async fn import_money_secrets( &self, secrets: Vec, output: &mut Vec, ) -> Result> { let existing_secrets = self.get_money_secrets().await?; let mut ret = Vec::with_capacity(secrets.len()); for secret in secrets { // Check if secret already exists if existing_secrets.contains(&secret) { output.push(format!("Existing key found: {secret}")); continue } ret.push(PublicKey::from_secret(secret)); let is_default = 0; let public = serialize_async(&PublicKey::from_secret(secret)).await; let secret = serialize_async(&secret).await; let query = format!( "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET ); if let Err(e) = self.wallet.exec_sql(&query, params![is_default, public, secret]).await { return Err(Error::DatabaseError(format!( "[import_money_secrets] Inserting new address failed: {e}" ))) } } Ok(ret) } /// Fetch known unspent balances from the wallet and return them as a hashmap. pub async fn money_balance(&self) -> Result> { let mut coins = self.get_coins(false).await?; coins.retain(|x| x.0.note.spend_hook == FuncId::none()); // Fill this map with balances let mut balmap: HashMap = HashMap::new(); for coin in coins { let mut value = coin.0.note.value; if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) { value += prev; } balmap.insert(coin.0.note.token_id.to_string(), value); } Ok(balmap) } /// Fetch all coins and their metadata related to the Money contract from the wallet. /// Optionally also fetch spent ones. /// The boolean in the returned tuple notes if the coin was marked /// as spent, along with the height and tx it was spent in. pub async fn get_coins( &self, fetch_spent: bool, ) -> Result, String)>> { let query = if fetch_spent { self.wallet.query_multiple(&MONEY_COINS_TABLE, &[], vec![]).await } else { self.wallet .query_multiple( &MONEY_COINS_TABLE, &[], convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false)}, ) .await }; let rows = match query { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!("[get_coins] Coins retrieval failed: {e}"))) } }; let mut owncoins = Vec::with_capacity(rows.len()); for row in rows { owncoins.push(self.parse_coin_record(&row).await?) } Ok(owncoins) } /// Fetch provided token unspend balances from the wallet. pub async fn get_token_coins(&self, token_id: &TokenId) -> Result> { let query = self .wallet .query_multiple( &MONEY_COINS_TABLE, &[], convert_named_params! { (MONEY_COINS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(&FuncId::none()).await), (MONEY_COINS_COL_IS_SPENT, false), }, ) .await; let rows = match query { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_token_coins] Coins retrieval failed: {e}" ))) } }; let mut owncoins = Vec::with_capacity(rows.len()); for row in rows { owncoins.push(self.parse_coin_record(&row).await?.0) } Ok(owncoins) } /// Fetch provided contract specified token unspend balances from the wallet. pub async fn get_contract_token_coins( &self, token_id: &TokenId, spend_hook: &FuncId, user_data: &pallas::Base, ) -> Result> { let query = self .wallet .query_multiple( &MONEY_COINS_TABLE, &[], convert_named_params! { (MONEY_COINS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(spend_hook).await), (MONEY_COINS_COL_USER_DATA, serialize_async(user_data).await), (MONEY_COINS_COL_IS_SPENT, false), }, ) .await; let rows = match query { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_contract_token_coins] Coins retrieval failed: {e}" ))) } }; let mut owncoins = Vec::with_capacity(rows.len()); for row in rows { owncoins.push(self.parse_coin_record(&row).await?.0) } Ok(owncoins) } /// Auxiliary function to parse a `MONEY_COINS_TABLE` record. /// The boolean in the returned tuple notes if the coin was marked /// as spent, along with the height and tx it was spent in. async fn parse_coin_record( &self, row: &[Value], ) -> Result<(OwnCoin, u32, bool, Option, String)> { let Value::Blob(ref coin_bytes) = row[0] else { return Err(Error::ParseFailed("[parse_coin_record] Coin bytes parsing failed")) }; let coin: Coin = deserialize_async(coin_bytes).await?; let Value::Blob(ref value_bytes) = row[1] else { return Err(Error::ParseFailed("[parse_coin_record] Value bytes parsing failed")) }; let value: u64 = deserialize_async(value_bytes).await?; let Value::Blob(ref token_id_bytes) = row[2] else { return Err(Error::ParseFailed("[parse_coin_record] Token ID bytes parsing failed")) }; let token_id: TokenId = deserialize_async(token_id_bytes).await?; let Value::Blob(ref spend_hook_bytes) = row[3] else { return Err(Error::ParseFailed("[parse_coin_record] Spend hook bytes parsing failed")) }; let spend_hook: pallas::Base = deserialize_async(spend_hook_bytes).await?; let Value::Blob(ref user_data_bytes) = row[4] else { return Err(Error::ParseFailed("[parse_coin_record] User data bytes parsing failed")) }; let user_data: pallas::Base = deserialize_async(user_data_bytes).await?; let Value::Blob(ref coin_blind_bytes) = row[5] else { return Err(Error::ParseFailed("[parse_coin_record] Coin blind bytes parsing failed")) }; let coin_blind: BaseBlind = deserialize_async(coin_blind_bytes).await?; let Value::Blob(ref value_blind_bytes) = row[6] else { return Err(Error::ParseFailed("[parse_coin_record] Value blind bytes parsing failed")) }; let value_blind: ScalarBlind = deserialize_async(value_blind_bytes).await?; let Value::Blob(ref token_blind_bytes) = row[7] else { return Err(Error::ParseFailed("[parse_coin_record] Token blind bytes parsing failed")) }; let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?; let Value::Blob(ref secret_bytes) = row[8] else { return Err(Error::ParseFailed("[parse_coin_record] Secret bytes parsing failed")) }; let secret: SecretKey = deserialize_async(secret_bytes).await?; let Value::Blob(ref leaf_position_bytes) = row[9] else { return Err(Error::ParseFailed("[parse_coin_record] Leaf position bytes parsing failed")) }; let leaf_position: Position = deserialize_async(leaf_position_bytes).await?; let Value::Blob(ref memo) = row[10] else { return Err(Error::ParseFailed("[parse_coin_record] Memo parsing failed")) }; let Value::Integer(creation_height) = row[11] else { return Err(Error::ParseFailed("[parse_coin_record] Creation height parsing failed")) }; let Ok(creation_height) = u32::try_from(creation_height) else { return Err(Error::ParseFailed("[parse_coin_record] Creation height parsing failed")) }; let Value::Integer(is_spent) = row[12] else { return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed")) }; let Ok(is_spent) = u64::try_from(is_spent) else { return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed")) }; let is_spent = is_spent > 0; let spent_height = match row[13] { Value::Integer(spent_height) => { let Ok(spent_height) = u32::try_from(spent_height) else { return Err(Error::ParseFailed( "[parse_coin_record] Spent height parsing failed", )) }; Some(spent_height) } Value::Null => None, _ => return Err(Error::ParseFailed("[parse_coin_record] Spent height parsing failed")), }; let Value::Text(ref spent_tx_hash) = row[14] else { return Err(Error::ParseFailed( "[parse_coin_record] Spent transaction hash parsing failed", )) }; let note = MoneyNote { value, token_id, spend_hook: spend_hook.into(), user_data, coin_blind, value_blind, token_blind, memo: memo.clone(), }; Ok(( OwnCoin { coin, note, secret, leaf_position }, creation_height, is_spent, spent_height, spent_tx_hash.clone(), )) } /// Create an alias record for provided Token ID. pub async fn add_alias( &self, alias: String, token_id: TokenId, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Generating alias {alias} for Token: {token_id}")); let query = format!( "INSERT OR REPLACE INTO {} ({}, {}) VALUES (?1, ?2);", *MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS, MONEY_ALIASES_COL_TOKEN_ID, ); self.wallet .exec_sql( &query, params![serialize_async(&alias).await, serialize_async(&token_id).await], ) .await } /// Fetch all aliases from the wallet. /// Optionally filter using alias name and/or token id. pub async fn get_aliases( &self, alias_filter: Option, token_id_filter: Option, ) -> Result> { let rows = match self.wallet.query_multiple(&MONEY_ALIASES_TABLE, &[], vec![]).await { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_aliases] Aliases retrieval failed: {e}" ))) } }; // Fill this map with aliases let mut map: HashMap = HashMap::new(); for row in rows { let Value::Blob(ref alias_bytes) = row[0] else { return Err(Error::ParseFailed("[get_aliases] Alias bytes parsing failed")) }; let alias: String = deserialize_async(alias_bytes).await?; if alias_filter.is_some() && alias_filter.as_ref().unwrap() != &alias { continue } let Value::Blob(ref token_id_bytes) = row[1] else { return Err(Error::ParseFailed("[get_aliases] TokenId bytes parsing failed")) }; let token_id: TokenId = deserialize_async(token_id_bytes).await?; if token_id_filter.is_some() && token_id_filter.as_ref().unwrap() != &token_id { continue } map.insert(alias, token_id); } Ok(map) } /// Fetch all aliases from the wallet, mapped by token id. pub async fn get_aliases_mapped_by_token(&self) -> Result> { let aliases = self.get_aliases(None, None).await?; let mut map: HashMap = HashMap::new(); for (alias, token_id) in aliases { let aliases_string = if let Some(prev) = map.get(&token_id.to_string()) { format!("{prev}, {alias}") } else { alias }; map.insert(token_id.to_string(), aliases_string); } Ok(map) } /// Remove provided alias record from the wallet database. pub async fn remove_alias( &self, alias: String, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Removing alias: {alias}")); let query = format!( "DELETE FROM {} WHERE {} = ?1;", *MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS, ); self.wallet.exec_sql(&query, params![serialize_async(&alias).await]).await } /// Mark a given coin in the wallet as unspent. pub async fn unspend_coin(&self, coin: &Coin) -> WalletDbResult<()> { let query = format!( "UPDATE {} SET {} = 0, {} = NULL, {} = '-' WHERE {} = ?1;", *MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_SPENT_HEIGHT, MONEY_COINS_COL_SPENT_TX_HASH, MONEY_COINS_COL_COIN ); self.wallet.exec_sql(&query, params![serialize_async(&coin.inner()).await]).await } /// Fetch the Money Merkle tree from the cache. /// If it doesn't exists a new Merkle Tree is returned. pub async fn get_money_tree(&self) -> Result { match self.cache.merkle_trees.get(SLED_MERKLE_TREES_MONEY)? { Some(tree_bytes) => Ok(deserialize_async(&tree_bytes).await?), None => { let mut tree = MerkleTree::new(u32::MAX as usize); tree.append(MerkleNode::from(pallas::Base::ZERO)); let _ = tree.mark().unwrap(); Ok(tree) } } } /// Auxiliary function to grab all the nullifiers, coins with their /// notes and a flag indicating if its a block reward, and freezes /// from a transaction money call. async fn parse_money_call( &self, scan_cache: &mut ScanCache, call_idx: &usize, calls: &[DarkLeaf], ) -> Result<(Vec, Vec<(Coin, AeadEncryptedNote, bool)>, Vec)> { let mut nullifiers: Vec = vec![]; let mut coins: Vec<(Coin, AeadEncryptedNote, bool)> = vec![]; let mut freezes: Vec = vec![]; let call = &calls[*call_idx]; let data = &call.data.data; match MoneyFunction::try_from(data[0])? { MoneyFunction::FeeV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::FeeV1 call")); let params: MoneyFeeParamsV1 = deserialize_async(&data[9..]).await?; nullifiers.push(params.input.nullifier); if !params.output.tx_local { coins.push((params.output.coin, params.output.note, false)); } } MoneyFunction::GenesisMintV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::GenesisMintV1 call")); let params: MoneyGenesisMintParamsV1 = deserialize_async(&data[1..]).await?; for output in params.outputs { if !output.tx_local { coins.push((output.coin, output.note, false)); } } } MoneyFunction::PoWRewardV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::PoWRewardV1 call")); let params: MoneyPoWRewardParamsV1 = deserialize_async(&data[1..]).await?; if !params.output.tx_local { coins.push((params.output.coin, params.output.note, true)); } } MoneyFunction::TransferV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::TransferV1 call")); let params: MoneyTransferParamsV1 = deserialize_async(&data[1..]).await?; for input in params.inputs { nullifiers.push(input.nullifier); } for output in params.outputs { if !output.tx_local { coins.push((output.coin, output.note, false)); } } } MoneyFunction::AuthTokenMintV1 => { scan_cache .log(String::from("[parse_money_call] Found Money::AuthTokenMintV1 call")); // Handled in TokenMint } MoneyFunction::AuthTokenFreezeV1 => { scan_cache .log(String::from("[parse_money_call] Found Money::AuthTokenFreezeV1 call")); let params: MoneyAuthTokenFreezeParamsV1 = deserialize_async(&data[1..]).await?; freezes.push(params.token_id); } MoneyFunction::TokenMintV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::TokenMintV1 call")); let params: MoneyTokenMintParamsV1 = deserialize_async(&data[1..]).await?; coins.push((params.coin, params.enc_note, false)) } MoneyFunction::BurnV1 => { scan_cache.log(String::from("[parse_money_call] Found Money::BurnV1 call")); let params: MoneyBurnParamsV1 = deserialize_async(&data[1..]).await?; for input in params.inputs { nullifiers.push(input.nullifier); } } } Ok((nullifiers, coins, freezes)) } /// Auxiliary function to handle coins with their notes and flag /// indicating if its a block reward from a transaction money call. /// Returns our found own coins along with the block signing key, /// if found. fn handle_money_call_coins( &self, tree: &mut MerkleTree, secrets: &[SecretKey], messages_buffer: &mut Vec, coins: &[(Coin, AeadEncryptedNote, bool)], ) -> Result<(Vec, Option)> { // Keep track of our own coins found in the vec let mut owncoins = vec![]; // Check if provided coins vec is empty if coins.is_empty() { return Ok((owncoins, None)) } // Handle provided coins vector and grab our own, // along with the block signing key if its a block // reward coin. Only one reward call and coin exists // in each block. let mut block_signing_key = None; for (coin, note, is_block_reward) in coins { // Append the new coin to the Merkle tree. // Every coin has to be added. tree.append(MerkleNode::from(coin.inner())); // Attempt to decrypt the note for secret in secrets { let Ok(note) = note.decrypt::(secret) else { continue }; messages_buffer.push(String::from( "[handle_money_call_coins] Successfully decrypted a Money Note", )); messages_buffer .push(String::from("[handle_money_call_coins] Witnessing coin in Merkle tree")); let leaf_position = tree.mark().unwrap(); if *is_block_reward { messages_buffer .push(String::from("[handle_money_call_coins] Grabing block signing key")); block_signing_key = Some(deserialize(¬e.memo)?); } let owncoin = OwnCoin { coin: *coin, note, secret: *secret, leaf_position }; owncoins.push(owncoin); break } } Ok((owncoins, block_signing_key)) } /// Auxiliary function to handle own coins from a transaction money /// call. async fn handle_money_call_owncoins( &self, scan_cache: &mut ScanCache, coins: &[OwnCoin], creation_height: &u32, ) -> Result<()> { scan_cache.log(format!("Found {} OwnCoin(s) in transaction", coins.len())); // Check if we have any owncoins to process if coins.is_empty() { return Ok(()) } // This is the SQL query we'll be executing to insert new coins into the wallet let query = format!( "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14);", *MONEY_COINS_TABLE, MONEY_COINS_COL_COIN, MONEY_COINS_COL_VALUE, MONEY_COINS_COL_TOKEN_ID, MONEY_COINS_COL_SPEND_HOOK, MONEY_COINS_COL_USER_DATA, MONEY_COINS_COL_COIN_BLIND, MONEY_COINS_COL_VALUE_BLIND, MONEY_COINS_COL_TOKEN_BLIND, MONEY_COINS_COL_SECRET, MONEY_COINS_COL_LEAF_POSITION, MONEY_COINS_COL_MEMO, MONEY_COINS_COL_CREATION_HEIGHT, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_SPENT_HEIGHT, ); // Handle our own coins let spent_height: Option = None; for coin in coins { scan_cache.log(format!("OwnCoin: {:?}", coin.coin)); // Grab coin record key let key = coin.coin.to_bytes(); // Push to our own coins nullifiers cache scan_cache .owncoins_nullifiers .insert(coin.nullifier().to_bytes(), (key, coin.leaf_position)); // Execute the query let params = params![ key.to_vec(), serialize(&coin.note.value), serialize(&coin.note.token_id), serialize(&coin.note.spend_hook), serialize(&coin.note.user_data), serialize(&coin.note.coin_blind), serialize(&coin.note.value_blind), serialize(&coin.note.token_blind), serialize(&coin.secret), serialize(&coin.leaf_position), serialize(&coin.note.memo), *creation_height, 0, // <-- is_spent spent_height, ]; if let Err(e) = self.wallet.exec_sql(&query, params).await { return Err(Error::DatabaseError(format!( "[handle_money_call_owncoins] Inserting Money coin failed: {e}" ))) } } Ok(()) } /// Auxiliary function to handle freezes from a transaction money /// call. /// Returns a flag indicating if provided freezes refer to our own /// wallet. async fn handle_money_call_freezes( &self, own_tokens: &[TokenId], freezes: &[TokenId], freeze_height: &u32, ) -> Result { // Check if we have any freezes to process if freezes.is_empty() { return Ok(false) } // Find our own tokens that got frozen let mut own_freezes = Vec::with_capacity(freezes.len()); for freeze in freezes { if own_tokens.contains(freeze) { own_freezes.push(freeze); } } // Check if we need to freeze anything if own_freezes.is_empty() { return Ok(false) } // This is the SQL query we'll be executing to update frozen tokens into the wallet let query = format!( "UPDATE {} SET {} = 1, {} = ?1 WHERE {} = ?2;", *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_FREEZE_HEIGHT, MONEY_TOKENS_COL_TOKEN_ID, ); for token_id in own_freezes { // Grab token record key let key = serialize_async(token_id).await; // Execute the query if let Err(e) = self.wallet.exec_sql(&query, params![Some(*freeze_height), key]).await { return Err(Error::DatabaseError(format!( "[handle_money_call_freezes] Update Money token freeze failed: {e}" ))) } } Ok(true) } /// Append data related to Money contract transactions into the /// wallet database and update the provided scan cache. /// Returns a flag indicating if provided data refer to our own /// wallet along with the block signing key, if found. pub async fn apply_tx_money_data( &self, scan_cache: &mut ScanCache, call_idx: &usize, calls: &[DarkLeaf], tx_hash: &str, block_height: &u32, ) -> Result<(bool, Option)> { // Parse the call let (nullifiers, coins, freezes) = self.parse_money_call(scan_cache, call_idx, calls).await?; // Parse call coins and grab our own let (owncoins, block_signing_key) = self.handle_money_call_coins( &mut scan_cache.money_tree, &scan_cache.notes_secrets, &mut scan_cache.messages_buffer, &coins, )?; // Update nullifiers smt self.smt_insert(&mut scan_cache.money_smt, &nullifiers)?; // Check if we have any spent coins let wallet_spent_coins = self .mark_spent_coins( Some(&mut scan_cache.money_tree), &scan_cache.owncoins_nullifiers, &nullifiers, &Some(*block_height), tx_hash, ) .await?; // Handle our own coins self.handle_money_call_owncoins(scan_cache, &owncoins, block_height).await?; // Handle freezes let wallet_freezes = self.handle_money_call_freezes(&scan_cache.own_tokens, &freezes, block_height).await?; if self.fun && !owncoins.is_empty() { kaching().await; } Ok((wallet_spent_coins || !owncoins.is_empty() || wallet_freezes, block_signing_key)) } /// Auxiliary function to grab all the nullifiers from a transaction money call. async fn money_call_nullifiers(&self, call: &DarkLeaf) -> Result> { let mut nullifiers: Vec = vec![]; let data = &call.data.data; match MoneyFunction::try_from(data[0])? { MoneyFunction::FeeV1 => { let params: MoneyFeeParamsV1 = deserialize_async(&data[9..]).await?; nullifiers.push(params.input.nullifier); } MoneyFunction::TransferV1 => { let params: MoneyTransferParamsV1 = deserialize_async(&data[1..]).await?; for input in params.inputs { nullifiers.push(input.nullifier); } } _ => { /* Do nothing */ } } Ok(nullifiers) } /// Mark provided transaction input coins as spent. pub async fn mark_tx_spend(&self, tx: &Transaction, output: &mut Vec) -> Result<()> { // Create a cache of all our own nullifiers let mut owncoins_nullifiers = BTreeMap::new(); for coin in self.get_coins(true).await? { owncoins_nullifiers.insert( coin.0.nullifier().to_bytes(), (coin.0.coin.to_bytes(), coin.0.leaf_position), ); } let tx_hash = tx.hash().to_string(); output.push(format!("[mark_tx_spend] Processing transaction: {tx_hash}")); for (i, call) in tx.calls.iter().enumerate() { if call.data.contract_id != *MONEY_CONTRACT_ID { continue } output.push(format!("[mark_tx_spend] Found Money contract in call {i}")); let nullifiers = self.money_call_nullifiers(call).await?; self.mark_spent_coins(None, &owncoins_nullifiers, &nullifiers, &None, &tx_hash).await?; } Ok(()) } /// Marks all coins in the wallet as spent, if their nullifier is in the given set. /// Returns a flag indicating if any of the provided nullifiers refer to our own wallet. pub async fn mark_spent_coins( &self, mut tree: Option<&mut MerkleTree>, owncoins_nullifiers: &BTreeMap<[u8; 32], ([u8; 32], Position)>, nullifiers: &[Nullifier], spent_height: &Option, spent_tx_hash: &str, ) -> Result { if nullifiers.is_empty() { return Ok(false) } // Find our owncoins that where spent let mut spent_owncoins = Vec::new(); for nullifier in nullifiers { if let Some(coin) = owncoins_nullifiers.get(&nullifier.to_bytes()) { spent_owncoins.push(coin); } } if spent_owncoins.is_empty() { return Ok(false) } // Create an SQL `UPDATE` query to mark rows as spent(1) let query = format!( "UPDATE {} SET {} = 1, {} = ?1, {} = ?2 WHERE {} = ?3;", *MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_SPENT_HEIGHT, MONEY_COINS_COL_SPENT_TX_HASH, MONEY_COINS_COL_COIN ); // Mark spent own coins for (ownoin, leaf_position) in spent_owncoins { // Execute the query if let Err(e) = self .wallet .exec_sql(&query, params![*spent_height, spent_tx_hash, ownoin.to_vec()]) .await { return Err(Error::DatabaseError(format!( "[mark_spent_coins] Marking spent coin failed: {e}" ))) } // Remove the coin mark from the Merkle tree if let Some(ref mut tree) = tree { tree.remove_mark(*leaf_position); } } Ok(true) } /// Inserts given slice to the wallets nullifiers Sparse Merkle Tree. pub fn smt_insert(&self, smt: &mut CacheSmt, nullifiers: &[Nullifier]) -> Result<()> { let leaves: Vec<_> = nullifiers.iter().map(|x| (x.inner(), x.inner())).collect(); Ok(smt.insert_batch(leaves)?) } /// Reset the Money Merkle tree in the cache. pub fn reset_money_tree(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Resetting Money Merkle tree")); if let Err(e) = self.cache.merkle_trees.remove(SLED_MERKLE_TREES_MONEY) { output.push(format!("[reset_money_tree] Resetting Money Merkle tree failed: {e}")); return Err(WalletDbError::GenericError) } output.push(String::from("Successfully reset Money Merkle tree")); Ok(()) } /// Reset the Money nullifiers Sparse Merkle Tree in the cache. pub fn reset_money_smt(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Resetting Money Sparse Merkle tree")); if let Err(e) = self.cache.money_smt.clear() { output .push(format!("[reset_money_smt] Resetting Money Sparse Merkle tree failed: {e}")); return Err(WalletDbError::GenericError) } output.push(String::from("Successfully reset Money Sparse Merkle tree")); Ok(()) } /// Reset the Money coins in the wallet. pub async fn reset_money_coins(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Resetting coins")); let query = format!("DELETE FROM {};", *MONEY_COINS_TABLE); self.wallet.exec_sql(&query, vec![]).await?; output.push(String::from("Successfully reset coins")); Ok(()) } /// Remove the Money coins in the wallet that were created after /// provided height. pub async fn remove_money_coins_after( &self, height: &u32, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Removing coins after: {height}")); let query = format!( "DELETE FROM {} WHERE {} > ?1;", *MONEY_COINS_TABLE, MONEY_COINS_COL_CREATION_HEIGHT ); self.wallet.exec_sql(&query, params![*height]).await?; output.push(String::from("Successfully removed coins")); Ok(()) } /// Mark the Money coins in the wallet that were spent after /// provided height as unspent. pub async fn unspent_money_coins_after( &self, height: &u32, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Unspenting coins after: {height}")); let query = format!( "UPDATE {} SET {} = 0, {} = NULL, {} = '=' WHERE {} > ?1;", *MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_SPENT_HEIGHT, MONEY_COINS_COL_SPENT_TX_HASH, MONEY_COINS_COL_SPENT_HEIGHT ); self.wallet.exec_sql(&query, params![Some(*height)]).await?; output.push(String::from("Successfully unspent coins")); Ok(()) } /// Retrieve token by provided string. /// Input string represents either an alias or a token id. pub async fn get_token(&self, input: String) -> Result { // Check if input is an alias(max 5 characters) if input.chars().count() <= 5 { let aliases = self.get_aliases(Some(input.clone()), None).await?; if let Some(token_id) = aliases.get(&input) { return Ok(*token_id) } } // Else parse input Ok(TokenId::from_str(input.as_str())?) } /// Create and append a `Money::Fee` call to a given [`Transaction`]. /// /// Optionally takes a set of spent coins in order not to reuse them here. /// /// Returns the `Fee` call, and all necessary data and parameters related. pub async fn append_fee_call( &self, tx: &Transaction, money_merkle_tree: &MerkleTree, fee_pk: &ProvingKey, fee_zkbin: &ZkBinary, spent_coins: Option<&[OwnCoin]>, ) -> Result<(ContractCall, Vec, Vec)> { // First we verify the fee-less transaction to see how much fee it requires for execution // and verification. let required_fee = compute_fee(&FEE_CALL_GAS) + self.get_tx_fee(tx, false).await?; // Knowing the total gas, we can now find an OwnCoin of enough value // so that we can create a valid Money::Fee call. let mut available_coins = self.get_token_coins(&DARK_TOKEN_ID).await?; available_coins.retain(|x| x.note.value > required_fee); if let Some(spent_coins) = spent_coins { available_coins.retain(|x| !spent_coins.contains(x)); } if available_coins.is_empty() { return Err(Error::Custom("Not enough native tokens to pay for fees".to_string())) } let coin = &available_coins[0]; let change_value = coin.note.value - required_fee; // Input and output setup let input = FeeCallInput { coin: coin.clone(), merkle_path: money_merkle_tree.witness(coin.leaf_position, 0).unwrap(), user_data_blind: BaseBlind::random(&mut OsRng), }; let output = FeeCallOutput { public_key: PublicKey::from_secret(coin.secret), value: change_value, token_id: coin.note.token_id, blind: BaseBlind::random(&mut OsRng), spend_hook: FuncId::none(), user_data: pallas::Base::ZERO, }; // Create blinding factors let token_blind = BaseBlind::random(&mut OsRng); let input_value_blind = ScalarBlind::random(&mut OsRng); let fee_value_blind = ScalarBlind::random(&mut OsRng); let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]); // Create an ephemeral signing key let signature_secret = SecretKey::random(&mut OsRng); // Create the actual fee proof let (proof, public_inputs) = create_fee_proof( fee_zkbin, fee_pk, &input, input_value_blind, &output, output_value_blind, output.spend_hook, output.user_data, output.blind, token_blind, signature_secret, )?; // Encrypted note for the output let note = MoneyNote { coin_blind: output.blind, value: output.value, token_id: output.token_id, spend_hook: output.spend_hook, user_data: output.user_data, value_blind: output_value_blind, token_blind, memo: vec![], }; let encrypted_note = AeadEncryptedNote::encrypt(¬e, &output.public_key, &mut OsRng)?; let params = MoneyFeeParamsV1 { input: Input { value_commit: public_inputs.input_value_commit, token_commit: public_inputs.token_commit, nullifier: public_inputs.nullifier, merkle_root: public_inputs.merkle_root, user_data_enc: public_inputs.input_user_data_enc, signature_public: public_inputs.signature_public, tx_local: false, }, output: Output { value_commit: public_inputs.output_value_commit, token_commit: public_inputs.token_commit, coin: public_inputs.output_coin, note: encrypted_note, tx_local: false, }, fee_value_blind, token_blind, }; // Encode the contract call let mut data = vec![MoneyFunction::FeeV1 as u8]; required_fee.encode_async(&mut data).await?; params.encode_async(&mut data).await?; let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; Ok((call, vec![proof], vec![signature_secret])) } /// Create and attach the fee call to given transaction. pub async fn attach_fee(&self, tx: &mut Transaction) -> Result<()> { // Grab spent coins nullifiers of the transactions and check no other fee call exists let mut tx_nullifiers = vec![]; for call in &tx.calls { if call.data.contract_id != *MONEY_CONTRACT_ID { continue } match MoneyFunction::try_from(call.data.data[0])? { MoneyFunction::FeeV1 => { return Err(Error::Custom("Fee call already exists".to_string())) } _ => { /* Do nothing */ } } let nullifiers = self.money_call_nullifiers(call).await?; tx_nullifiers.extend_from_slice(&nullifiers); } // Grab all native owncoins to check if any is spent let mut spent_coins = vec![]; let available_coins = self.get_token_coins(&DARK_TOKEN_ID).await?; for coin in available_coins { if tx_nullifiers.contains(&coin.nullifier()) { spent_coins.push(coin); } } // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) else { return Err(Error::Custom("Fee circuit not found".to_string())) }; let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?; let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); // Creating Fee circuits proving keys let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); // We first have to execute the fee-less tx to gather its used gas, and then we feed // it into the fee-creating function. let tree = self.get_money_tree().await?; let (fee_call, fee_proofs, fee_secrets) = self.append_fee_call(tx, &tree, &fee_pk, &fee_zkbin, Some(&spent_coins)).await?; // Append the fee call to the transaction tx.calls.push(DarkLeaf { data: fee_call, parent_index: None, children_indexes: vec![] }); tx.proofs.push(fee_proofs); let sigs = tx.create_sigs(&fee_secrets)?; tx.signatures.push(sigs); Ok(()) } }