|
|
@@ -15,18 +15,10 @@
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
*/
|
|
|
-
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
-use darkfi::{rpc::jsonrpc::JsonRequest, util::parse::encode_base10, wallet::walletdb::QueryType};
|
|
|
-use darkfi_dao_contract::dao_client::{
|
|
|
- DAO_DAOS_COL_APPROVAL_RATIO_BASE, DAO_DAOS_COL_APPROVAL_RATIO_QUOT, DAO_DAOS_COL_BULLA_BLIND,
|
|
|
- DAO_DAOS_COL_CALL_INDEX, DAO_DAOS_COL_DAO_ID, DAO_DAOS_COL_GOV_TOKEN_ID,
|
|
|
- DAO_DAOS_COL_LEAF_POSITION, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PROPOSER_LIMIT,
|
|
|
- DAO_DAOS_COL_QUORUM, DAO_DAOS_COL_SECRET, DAO_DAOS_COL_TX_HASH, DAO_DAOS_TABLE,
|
|
|
- DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE,
|
|
|
-};
|
|
|
+use darkfi::{rpc::jsonrpc::JsonRequest, wallet::walletdb::QueryType};
|
|
|
use darkfi_money_contract::client::{
|
|
|
Coin, Note, OwnCoin, MONEY_COINS_COL_COIN, MONEY_COINS_COL_COIN_BLIND,
|
|
|
MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_LEAF_POSITION, MONEY_COINS_COL_MEMO,
|
|
|
@@ -34,29 +26,23 @@ use darkfi_money_contract::client::{
|
|
|
MONEY_COINS_COL_SPEND_HOOK, MONEY_COINS_COL_TOKEN_BLIND, MONEY_COINS_COL_TOKEN_ID,
|
|
|
MONEY_COINS_COL_USER_DATA, MONEY_COINS_COL_VALUE, MONEY_COINS_COL_VALUE_BLIND,
|
|
|
MONEY_COINS_TABLE, MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE,
|
|
|
- MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE,
|
|
|
- MONEY_TREE_COL_TREE, MONEY_TREE_TABLE,
|
|
|
+ MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_KEY_ID, MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET,
|
|
|
+ MONEY_KEYS_TABLE, MONEY_TREE_COL_TREE, MONEY_TREE_TABLE,
|
|
|
};
|
|
|
use darkfi_sdk::{
|
|
|
- crypto::{
|
|
|
- constants::MERKLE_DEPTH, Keypair, MerkleNode, MerkleTree, Nullifier, PublicKey, SecretKey,
|
|
|
- TokenId,
|
|
|
- },
|
|
|
+ crypto::{Keypair, MerkleTree, Nullifier, PublicKey, SecretKey, TokenId},
|
|
|
incrementalmerkletree,
|
|
|
- incrementalmerkletree::bridgetree::BridgeTree,
|
|
|
pasta::pallas,
|
|
|
};
|
|
|
use darkfi_serial::{deserialize, serialize};
|
|
|
-use prettytable::{format, row, Table};
|
|
|
use rand::rngs::OsRng;
|
|
|
use serde_json::json;
|
|
|
|
|
|
use super::Drk;
|
|
|
-use crate::dao::Dao;
|
|
|
|
|
|
impl Drk {
|
|
|
- /// Initialize wallet with tables for the Money Contract.
|
|
|
- async fn wallet_initialize_money(&self) -> Result<()> {
|
|
|
+ /// Initialize wallet with tables for the Money contract
|
|
|
+ pub async fn initialize_money(&self) -> Result<()> {
|
|
|
let wallet_schema = include_str!("../../../src/contract/money/wallet.sql");
|
|
|
|
|
|
// We perform a request to darkfid with the schema to initialize
|
|
|
@@ -65,21 +51,21 @@ impl Drk {
|
|
|
let rep = self.rpc_client.request(req).await?;
|
|
|
|
|
|
if rep == true {
|
|
|
- println!("Successfully initialized wallet schema for the Money Contract");
|
|
|
+ eprintln!("Successfully initialized wallet schema for the Money contract");
|
|
|
} else {
|
|
|
- println!("Got unxpected reply from darkfid: {}", rep);
|
|
|
+ eprintln!("[initialize_money] Got unexpected reply from darkfid: {}", rep);
|
|
|
}
|
|
|
|
|
|
// Check if we have to initialize the Merkle tree.
|
|
|
- // We check if we find a row in the tree table, and if not, we create
|
|
|
- // a new tree and push it into the table.
|
|
|
+ // We check if we find a row in the tree table, and if not, we create a
|
|
|
+ // new tree and push it into the table.
|
|
|
let mut tree_needs_init = false;
|
|
|
- let query = format!("SELECT * FROM {}", MONEY_TREE_TABLE);
|
|
|
+ let query = format!("SELECT {} FROM {}", MONEY_TREE_COL_TREE, MONEY_TREE_TABLE);
|
|
|
let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]);
|
|
|
let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
|
|
|
- // For now, on success, we don't care what's returned, but maybe in
|
|
|
- // the future we should actually check it?
|
|
|
+ // For now, on success, we don't care what's returned, but in the future
|
|
|
+ // we should actually check it.
|
|
|
// TODO: The RPC needs a better variant for errors so detailed inspection
|
|
|
// can be done with error codes and all that.
|
|
|
if (self.rpc_client.request(req).await).is_err() {
|
|
|
@@ -87,17 +73,20 @@ impl Drk {
|
|
|
}
|
|
|
|
|
|
if tree_needs_init {
|
|
|
- println!("Initializing Merkle tree");
|
|
|
+ eprintln!("Initializing Money Merkle tree");
|
|
|
let tree = MerkleTree::new(100);
|
|
|
self.put_money_tree(&tree).await?;
|
|
|
- println!("Successfully initialized Merkle tree for Money Contract");
|
|
|
+ eprintln!("Successfully initialized Merkle tree for the Money contract");
|
|
|
}
|
|
|
|
|
|
- if (self.wallet_last_scanned_slot().await).is_err() {
|
|
|
+ // We maintain the last scanned slot as part of the Money contract,
|
|
|
+ // but at this moment it is also somewhat applicable to DAO scans.
|
|
|
+ if (self.last_scanned_slot().await).is_err() {
|
|
|
let query = format!(
|
|
|
"INSERT INTO {} ({}) VALUES (?1);",
|
|
|
MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_SLOT
|
|
|
);
|
|
|
+
|
|
|
let params = json!([query, QueryType::Integer as u8, 0]);
|
|
|
let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
let _ = self.rpc_client.request(req).await?;
|
|
|
@@ -106,67 +95,15 @@ impl Drk {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Initialize wallet with tables for the DAO Contract.
|
|
|
- async fn wallet_initialize_dao(&self) -> Result<()> {
|
|
|
- let wallet_schema = include_str!("../../../src/contract/dao/wallet.sql");
|
|
|
-
|
|
|
- // We perform a request to darkfid with the schema to initialize
|
|
|
- // the necessary tables in the wallet.
|
|
|
- let req = JsonRequest::new("wallet.exec_sql", json!([wallet_schema]));
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- if rep == true {
|
|
|
- println!("Successfully initialized wallet schema for the DAO Contract");
|
|
|
- } else {
|
|
|
- println!("Got unxpected reply from darkfid: {}", rep);
|
|
|
- }
|
|
|
-
|
|
|
- // Check if we have to initialize the Merkle trees. We check if one exists,
|
|
|
- // but we actually have to create two.
|
|
|
- let mut tree_needs_init = false;
|
|
|
- let query = format!("SELECT {} FROM {}", DAO_TREES_COL_DAOS_TREE, DAO_TREES_TABLE);
|
|
|
- let params = json!([query, QueryType::Blob as u8, DAO_TREES_COL_DAOS_TREE]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
-
|
|
|
- // For now, on success, we don't care what's returned, but maybe in
|
|
|
- // the future we should actually check it?
|
|
|
- // TODO: The RPC needs a better variant for errors so detailed inspection
|
|
|
- // can be done with error codes and all that.
|
|
|
- if (self.rpc_client.request(req).await).is_err() {
|
|
|
- tree_needs_init = true;
|
|
|
- }
|
|
|
-
|
|
|
- if tree_needs_init {
|
|
|
- println!("Initializing DAO Merkle trees");
|
|
|
- let daos_tree = MerkleTree::new(100);
|
|
|
- let proposals_tree = MerkleTree::new(100);
|
|
|
- self.put_dao_trees(&daos_tree, &proposals_tree).await?;
|
|
|
- println!("Successfully initialized Merkle trees for DAO Contract");
|
|
|
- }
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Main orchestration for wallet initialization. Internally, it initializes
|
|
|
- /// the wallet structure for the Money contract and the DAO contract.
|
|
|
- /// This should be performed initially before doing other operations.
|
|
|
- pub async fn wallet_initialize(&self) -> Result<()> {
|
|
|
- self.wallet_initialize_money().await?;
|
|
|
- self.wallet_initialize_dao().await?;
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Generate a new wallet keypair and put it in the according wallet table.
|
|
|
- pub async fn wallet_keygen(&self) -> Result<()> {
|
|
|
- println!("Generating a new keypair");
|
|
|
+ /// Generate a new keypair and place it into the wallet.
|
|
|
+ pub async fn money_keygen(&self) -> Result<()> {
|
|
|
+ eprintln!("Generating a new keypair");
|
|
|
// TODO: We might want to have hierarchical deterministic key derivation.
|
|
|
let keypair = Keypair::random(&mut OsRng);
|
|
|
- let public = serialize(&keypair.public);
|
|
|
- let secret = serialize(&keypair.secret);
|
|
|
let is_default = 0;
|
|
|
|
|
|
let query = format!(
|
|
|
- "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3)",
|
|
|
+ "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
|
|
|
MONEY_KEYS_TABLE,
|
|
|
MONEY_KEYS_COL_IS_DEFAULT,
|
|
|
MONEY_KEYS_COL_PUBLIC,
|
|
|
@@ -178,28 +115,118 @@ impl Drk {
|
|
|
QueryType::Integer as u8,
|
|
|
is_default,
|
|
|
QueryType::Blob as u8,
|
|
|
- public,
|
|
|
+ serialize(&keypair.public),
|
|
|
QueryType::Blob as u8,
|
|
|
- secret,
|
|
|
+ serialize(&keypair.secret),
|
|
|
]);
|
|
|
|
|
|
let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
let rep = self.rpc_client.request(req).await?;
|
|
|
|
|
|
if rep == true {
|
|
|
- println!("Successfully added new keypair to wallet");
|
|
|
+ eprintln!("Successfully added new keypair to wallet");
|
|
|
} else {
|
|
|
- println!("Got unexpected reply from darkfid: {}", rep);
|
|
|
+ eprintln!("[money_keygen] Got unexpected reply from darkfid: {}", rep);
|
|
|
}
|
|
|
|
|
|
- println!("New address: {}", keypair.public);
|
|
|
+ eprintln!("New address:");
|
|
|
+ println!("{}", keypair.public);
|
|
|
+
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Fetch all coins and their metadata from the wallet, optionally also spent ones.
|
|
|
- /// The boolean in the return tuple marks if the coin is marked as spent.
|
|
|
- pub async fn wallet_coins(&self, fetch_spent: bool) -> Result<Vec<(OwnCoin, bool)>> {
|
|
|
- eprintln!("Fetching OwnCoins from wallet");
|
|
|
+ /// Fetch all secret keys from the wallet
|
|
|
+ pub async fn get_money_secrets(&self) -> Result<Vec<SecretKey>> {
|
|
|
+ let query = format!("SELECT {} FROM {};", MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE);
|
|
|
+ let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_SECRET]);
|
|
|
+ let req = JsonRequest::new("wallet.query_row_multi", params);
|
|
|
+ let rep = self.rpc_client.request(req).await?;
|
|
|
+
|
|
|
+ // The returned thing should be an array of found rows.
|
|
|
+ let Some(rows) = rep.as_array() else {
|
|
|
+ return Err(anyhow!("[get_money_secrets] Unexpected response from darkfid: {}", rep));
|
|
|
+ };
|
|
|
+
|
|
|
+ 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 secret_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
|
|
|
+ let secret = deserialize(&secret_bytes)?;
|
|
|
+ secrets.push(secret);
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(secrets)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Import given secret keys into the wallet.
|
|
|
+ /// The query uses INSERT, so 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<SecretKey>) -> Result<Vec<PublicKey>> {
|
|
|
+ let mut ret = Vec::with_capacity(secrets.len());
|
|
|
+
|
|
|
+ for secret in secrets {
|
|
|
+ ret.push(PublicKey::from_secret(secret));
|
|
|
+ let is_default = 0;
|
|
|
+ let public = serialize(&PublicKey::from_secret(secret));
|
|
|
+ let secret = serialize(&secret);
|
|
|
+
|
|
|
+ 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,
|
|
|
+ );
|
|
|
+
|
|
|
+ let params = json!([
|
|
|
+ query,
|
|
|
+ QueryType::Integer as u8,
|
|
|
+ is_default,
|
|
|
+ QueryType::Blob as u8,
|
|
|
+ public,
|
|
|
+ QueryType::Blob as u8,
|
|
|
+ secret,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
+ let _ = self.rpc_client.request(req).await?;
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(ret)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Fetch pubkeys from the wallet and return the requested index.
|
|
|
+ pub async fn wallet_address(&self, idx: u64) -> Result<PublicKey> {
|
|
|
+ let query = format!(
|
|
|
+ "SELECT {} FROM {} WHERE {} = {};",
|
|
|
+ MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_TABLE, MONEY_KEYS_COL_KEY_ID, idx
|
|
|
+ );
|
|
|
+
|
|
|
+ let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_PUBLIC]);
|
|
|
+ let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
+ let rep = self.rpc_client.request(req).await?;
|
|
|
+
|
|
|
+ let Some(arr) = rep.as_array() else {
|
|
|
+ return Err(anyhow!("[wallet_address] Unexpected response from darkfid: {}", rep))
|
|
|
+ };
|
|
|
+
|
|
|
+ if arr.len() != 1 {
|
|
|
+ return Err(anyhow!("Did not find pubkey with index {}", idx))
|
|
|
+ }
|
|
|
+
|
|
|
+ let key_bytes: Vec<u8> = serde_json::from_value(arr[0].clone())?;
|
|
|
+ let public_key: PublicKey = deserialize(&key_bytes)?;
|
|
|
+
|
|
|
+ Ok(public_key)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 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.
|
|
|
+ pub async fn get_coins(&self, fetch_spent: bool) -> Result<Vec<(OwnCoin, bool)>> {
|
|
|
+ eprintln!("Fetching OwnCoins from the wallet");
|
|
|
+
|
|
|
let query = if fetch_spent {
|
|
|
format!("SELECT * FROM {}", MONEY_COINS_TABLE)
|
|
|
} else {
|
|
|
@@ -246,14 +273,14 @@ impl Drk {
|
|
|
|
|
|
// The returned thing should be an array of found rows.
|
|
|
let Some(rows) = rep.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
+ return Err(anyhow!("[get_coins] Unexpected response from darkfid: {}", rep))
|
|
|
};
|
|
|
|
|
|
- let mut owncoins = vec![];
|
|
|
+ let mut owncoins = Vec::with_capacity(rows.len());
|
|
|
|
|
|
for row in rows {
|
|
|
let Some(row) = row.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
+ return Err(anyhow!("[get_coins] Unexpected response from darkfid: {}", rep))
|
|
|
};
|
|
|
|
|
|
let coin_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
|
|
|
@@ -316,216 +343,6 @@ impl Drk {
|
|
|
Ok(owncoins)
|
|
|
}
|
|
|
|
|
|
- /// Fetch known balances from the wallet and try to print them as a table.
|
|
|
- pub async fn wallet_balance(&self) -> Result<()> {
|
|
|
- // This represents "false"
|
|
|
- let is_spent = 0;
|
|
|
-
|
|
|
- let query = format!(
|
|
|
- "SELECT {}, {} FROM {} WHERE {} = {}",
|
|
|
- MONEY_COINS_COL_VALUE,
|
|
|
- MONEY_COINS_COL_TOKEN_ID,
|
|
|
- MONEY_COINS_TABLE,
|
|
|
- MONEY_COINS_COL_IS_SPENT,
|
|
|
- is_spent,
|
|
|
- );
|
|
|
-
|
|
|
- let params = json!([
|
|
|
- query,
|
|
|
- QueryType::Blob as u8,
|
|
|
- MONEY_COINS_COL_VALUE,
|
|
|
- QueryType::Blob as u8,
|
|
|
- MONEY_COINS_COL_TOKEN_ID,
|
|
|
- ]);
|
|
|
-
|
|
|
- let req = JsonRequest::new("wallet.query_row_multi", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- // The returned thing should be an array of found rows.
|
|
|
- let Some(rows) = rep.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
- };
|
|
|
-
|
|
|
- // Fill this map with balances, and in the end we'll print it as a table.
|
|
|
- let mut balmap: HashMap<String, u64> = HashMap::new();
|
|
|
-
|
|
|
- // Let's scan through the rows and see if we got anything.
|
|
|
- // TODO: Separate tokens with spend-hook != 0
|
|
|
- for row in rows {
|
|
|
- let Some(row) = row.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
- };
|
|
|
-
|
|
|
- if row.len() != 2 {
|
|
|
- eprintln!("Error: Got invalid array, row should contain two elements.");
|
|
|
- eprintln!("Actual contents:\n:{:#?}", row);
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
- }
|
|
|
-
|
|
|
- let value_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
|
|
|
- let mut value: u64 = deserialize(&value_bytes)?;
|
|
|
-
|
|
|
- let token_bytes: Vec<u8> = serde_json::from_value(row[1].clone())?;
|
|
|
- let token_id: TokenId = deserialize(&token_bytes)?;
|
|
|
- let token_id = format!("{}", token_id);
|
|
|
-
|
|
|
- if let Some(prev) = balmap.get(&token_id) {
|
|
|
- value += prev;
|
|
|
- }
|
|
|
-
|
|
|
- balmap.insert(token_id, value);
|
|
|
- }
|
|
|
-
|
|
|
- // Create a prettytable with the new data.
|
|
|
- let mut table = Table::new();
|
|
|
- table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
|
- table.set_titles(row!["Token ID", "Balance"]);
|
|
|
-
|
|
|
- for (token_id, balance) in balmap.iter() {
|
|
|
- // FIXME: Don't hardcode to 8 decimals
|
|
|
- table.add_row(row![token_id, encode_base10(*balance, 8)]);
|
|
|
- }
|
|
|
-
|
|
|
- if table.is_empty() {
|
|
|
- eprintln!("No unspent balances found");
|
|
|
- } else {
|
|
|
- println!("{}", table);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Fetch pubkeys from the wallet and print the requested index.
|
|
|
- pub async fn wallet_address(&self, _idx: u64) -> Result<PublicKey> {
|
|
|
- let query = format!("SELECT {} FROM {};", MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_TABLE);
|
|
|
- let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_PUBLIC]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- let Some(arr) = rep.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep));
|
|
|
- };
|
|
|
-
|
|
|
- if arr.len() != 1 {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
- }
|
|
|
-
|
|
|
- let key_bytes: Vec<u8> = serde_json::from_value(arr[0].clone())?;
|
|
|
- let public_key: PublicKey = deserialize(&key_bytes)?;
|
|
|
-
|
|
|
- Ok(public_key)
|
|
|
- }
|
|
|
-
|
|
|
- /// Fetch secret keys from the wallet and return them if found.
|
|
|
- pub async fn wallet_secrets(&self) -> Result<Vec<SecretKey>> {
|
|
|
- let query = format!("SELECT {} FROM {};", MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE);
|
|
|
- let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_SECRET]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_multi", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- // The returned thing should be an array of found rows.
|
|
|
- let Some(rows) = rep.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep))
|
|
|
- };
|
|
|
-
|
|
|
- let mut secrets = vec![];
|
|
|
-
|
|
|
- // Let's scan through the rows and see if we got anything.
|
|
|
- for row in rows {
|
|
|
- let secret_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
|
|
|
- let secret: SecretKey = deserialize(&secret_bytes)?;
|
|
|
- secrets.push(secret);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(secrets)
|
|
|
- }
|
|
|
-
|
|
|
- /// Import given secret keys into the wallet. The query uses INSERT, so if the key already
|
|
|
- /// exists, it will simply be skipped.
|
|
|
- pub async fn wallet_import_secrets(&self, secrets: Vec<SecretKey>) -> Result<Vec<PublicKey>> {
|
|
|
- let mut ret = vec![];
|
|
|
-
|
|
|
- for secret in secrets {
|
|
|
- ret.push(PublicKey::from_secret(secret));
|
|
|
- let is_default = 0;
|
|
|
- let public = serialize(&PublicKey::from_secret(secret));
|
|
|
- let secret = serialize(&secret);
|
|
|
-
|
|
|
- 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,
|
|
|
- );
|
|
|
-
|
|
|
- let params = json!([
|
|
|
- query,
|
|
|
- QueryType::Integer as u8,
|
|
|
- is_default,
|
|
|
- QueryType::Blob as u8,
|
|
|
- public,
|
|
|
- QueryType::Blob as u8,
|
|
|
- secret,
|
|
|
- ]);
|
|
|
-
|
|
|
- let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- if rep != true {
|
|
|
- // Something weird happened?
|
|
|
- eprintln!("Got unexpected reply from darkfid: {}", rep);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Ok(ret)
|
|
|
- }
|
|
|
-
|
|
|
- /// Get the Money Merkle tree from the wallet
|
|
|
- pub async fn wallet_tree(&self) -> Result<BridgeTree<MerkleNode, MERKLE_DEPTH>> {
|
|
|
- let query = format!("SELECT * FROM {}", MONEY_TREE_TABLE);
|
|
|
- let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- let tree_bytes: Vec<u8> = serde_json::from_value(rep[0].clone())?;
|
|
|
- let tree = deserialize(&tree_bytes)?;
|
|
|
- Ok(tree)
|
|
|
- }
|
|
|
-
|
|
|
- pub async fn wallet_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> {
|
|
|
- let query = format!("SELECT * FROM {}", DAO_TREES_TABLE);
|
|
|
- let params = json!([
|
|
|
- query,
|
|
|
- QueryType::Blob as u8,
|
|
|
- DAO_TREES_COL_DAOS_TREE,
|
|
|
- QueryType::Blob as u8,
|
|
|
- DAO_TREES_COL_PROPOSALS_TREE
|
|
|
- ]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- let daos_tree_bytes: Vec<u8> = serde_json::from_value(rep[0].clone())?;
|
|
|
- let proposals_tree_bytes: Vec<u8> = serde_json::from_value(rep[1].clone())?;
|
|
|
-
|
|
|
- let daos_tree = deserialize(&daos_tree_bytes)?;
|
|
|
- let proposals_tree = deserialize(&proposals_tree_bytes)?;
|
|
|
-
|
|
|
- Ok((daos_tree, proposals_tree))
|
|
|
- }
|
|
|
-
|
|
|
- /// Get the last scanned slot from the wallet
|
|
|
- pub async fn wallet_last_scanned_slot(&self) -> Result<u64> {
|
|
|
- let query =
|
|
|
- format!("SELECT {} FROM {};", MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE);
|
|
|
-
|
|
|
- let params = json!([query, QueryType::Integer as u8, MONEY_INFO_COL_LAST_SCANNED_SLOT]);
|
|
|
- let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
- let rep = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- Ok(serde_json::from_value(rep[0].clone())?)
|
|
|
- }
|
|
|
-
|
|
|
/// Mark a coin in the wallet as spent
|
|
|
pub async fn mark_spent_coin(&self, coin: &Coin) -> Result<()> {
|
|
|
let query = format!(
|
|
|
@@ -547,14 +364,13 @@ impl Drk {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Marks all coins in the wallet as spent, if their nullifier is
|
|
|
- /// in the provided set
|
|
|
- pub async fn mark_spent_coins(&self, nullifiers: Vec<Nullifier>) -> Result<()> {
|
|
|
+ /// Marks all coins in the wallet as spent, if their nullifier is in the given set
|
|
|
+ pub async fn mark_spent_coins(&self, nullifiers: &[Nullifier]) -> Result<()> {
|
|
|
if nullifiers.is_empty() {
|
|
|
return Ok(())
|
|
|
}
|
|
|
|
|
|
- for (coin, _) in self.wallet_coins(false).await? {
|
|
|
+ for (coin, _) in self.get_coins(false).await? {
|
|
|
if nullifiers.contains(&coin.nullifier) {
|
|
|
self.mark_spent_coin(&coin.coin).await?;
|
|
|
}
|
|
|
@@ -567,7 +383,7 @@ impl Drk {
|
|
|
pub async fn unspend_coin(&self, coin: &Coin) -> Result<()> {
|
|
|
let query = format!(
|
|
|
"UPDATE {} SET {} = ?1 WHERE {} = ?2;",
|
|
|
- MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN
|
|
|
+ MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN,
|
|
|
);
|
|
|
|
|
|
let params = json!([
|
|
|
@@ -584,27 +400,45 @@ impl Drk {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Replace the Money Merkle tree in the wallet
|
|
|
- pub async fn put_money_tree(&self, tree: &BridgeTree<MerkleNode, MERKLE_DEPTH>) -> Result<()> {
|
|
|
+ /// Replace the Money Merkle tree in the wallet.
|
|
|
+ pub async fn put_money_tree(&self, tree: &MerkleTree) -> Result<()> {
|
|
|
let query = format!(
|
|
|
"DELETE FROM {}; INSERT INTO {} ({}) VALUES (?1);",
|
|
|
- MONEY_TREE_TABLE, MONEY_TREE_TABLE, MONEY_TREE_COL_TREE
|
|
|
+ MONEY_TREE_TABLE, MONEY_TREE_TABLE, MONEY_TREE_COL_TREE,
|
|
|
);
|
|
|
|
|
|
let params = json!([query, QueryType::Blob as u8, serialize(tree)]);
|
|
|
+
|
|
|
let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
let _ = self.rpc_client.request(req).await?;
|
|
|
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Reset the Money Contract Merkle tree and coins in the wallet
|
|
|
+ /// Fetch the Money Merkle tree from the wallet
|
|
|
+ pub async fn get_money_tree(&self) -> Result<MerkleTree> {
|
|
|
+ let query = format!("SELECT * FROM {}", MONEY_TREE_TABLE);
|
|
|
+ let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]);
|
|
|
+ let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
+ let rep = self.rpc_client.request(req).await?;
|
|
|
+
|
|
|
+ let tree_bytes: Vec<u8> = serde_json::from_value(rep[0].clone())?;
|
|
|
+ let tree = deserialize(&tree_bytes)?;
|
|
|
+ Ok(tree)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Reset the Money Merkle tree in the wallet
|
|
|
pub async fn reset_money_tree(&self) -> Result<()> {
|
|
|
eprintln!("Resetting Money Merkle tree");
|
|
|
- let tree = BridgeTree::<MerkleNode, MERKLE_DEPTH>::new(100);
|
|
|
+ let tree = MerkleTree::new(100);
|
|
|
self.put_money_tree(&tree).await?;
|
|
|
eprintln!("Successfully reset Money Merkle tree");
|
|
|
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Reset the Money coins in the wallet
|
|
|
+ pub async fn reset_money_coins(&self) -> Result<()> {
|
|
|
eprintln!("Resetting coins");
|
|
|
let query = format!("DELETE FROM {};", MONEY_COINS_TABLE);
|
|
|
let params = json!([query]);
|
|
|
@@ -615,171 +449,78 @@ impl Drk {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- /// Replace the DAO Merkle trees in the wallet
|
|
|
- pub async fn put_dao_trees(
|
|
|
- &self,
|
|
|
- daos_tree: &BridgeTree<MerkleNode, MERKLE_DEPTH>,
|
|
|
- proposals_tree: &BridgeTree<MerkleNode, MERKLE_DEPTH>,
|
|
|
- ) -> Result<()> {
|
|
|
+ /// Fetch known unspent balances from the wallet and return them as a hashmap.
|
|
|
+ pub async fn money_balance(&self) -> Result<HashMap<String, u64>> {
|
|
|
+ // This represents "false"
|
|
|
+ let is_spent = 0;
|
|
|
+
|
|
|
let query = format!(
|
|
|
- "DELETE FROM {}; INSERT INTO {} ({}, {}) VALUES (?1, ?2);",
|
|
|
- DAO_TREES_TABLE, DAO_TREES_TABLE, DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE
|
|
|
+ "SELECT {}, {} FROM {} WHERE {} = {}",
|
|
|
+ MONEY_COINS_COL_VALUE,
|
|
|
+ MONEY_COINS_COL_TOKEN_ID,
|
|
|
+ MONEY_COINS_TABLE,
|
|
|
+ MONEY_COINS_COL_IS_SPENT,
|
|
|
+ is_spent,
|
|
|
);
|
|
|
|
|
|
let params = json!([
|
|
|
query,
|
|
|
QueryType::Blob as u8,
|
|
|
- serialize(daos_tree),
|
|
|
- QueryType::Blob as u8,
|
|
|
- serialize(proposals_tree)
|
|
|
- ]);
|
|
|
-
|
|
|
- let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
-
|
|
|
- let _ = self.rpc_client.request(req).await?;
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Reset the DAO Contract Merkle trees in the wallet
|
|
|
- pub async fn reset_dao_trees(&self) -> Result<()> {
|
|
|
- eprintln!("Resetting DAO Merkle trees");
|
|
|
- let tree0 = MerkleTree::new(100);
|
|
|
- let tree1 = MerkleTree::new(100);
|
|
|
- self.put_dao_trees(&tree0, &tree1).await?;
|
|
|
- eprintln!("Successfully reset DAO Merkle trees");
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Write given DAOs into the wallet
|
|
|
- pub async fn put_daos(&self, daos: &[Dao]) -> Result<()> {
|
|
|
- for dao in daos {
|
|
|
- // Note that for now we just write the leaf pos, tx-hash, and call_index.
|
|
|
- // This is because we don't expect the other stuff to change.
|
|
|
- let query = format!(
|
|
|
- "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = ?4;",
|
|
|
- DAO_DAOS_TABLE,
|
|
|
- DAO_DAOS_COL_LEAF_POSITION,
|
|
|
- DAO_DAOS_COL_TX_HASH,
|
|
|
- DAO_DAOS_COL_CALL_INDEX,
|
|
|
- DAO_DAOS_COL_DAO_ID
|
|
|
- );
|
|
|
-
|
|
|
- let params = json!([
|
|
|
- query,
|
|
|
- QueryType::Blob as u8,
|
|
|
- serialize(&dao.leaf_position.unwrap()),
|
|
|
- QueryType::Blob as u8,
|
|
|
- serialize(&dao.tx_hash.unwrap()),
|
|
|
- QueryType::Integer as u8,
|
|
|
- dao.call_index.unwrap(),
|
|
|
- ]);
|
|
|
-
|
|
|
- let req = JsonRequest::new("wallet.exec_sql", params);
|
|
|
- let _ = self.rpc_client.request(req).await?;
|
|
|
- }
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- /// Fetch all DAOs from the wallet
|
|
|
- /// We use this a lot because we don't worry too much about performance in this
|
|
|
- /// tool, and also in practice probably not a lot of DAOs will be in a single
|
|
|
- /// wallet.
|
|
|
- pub async fn wallet_get_daos(&self) -> Result<Vec<Dao>> {
|
|
|
- let query = format!("SELECT * FROM {}", DAO_DAOS_TABLE);
|
|
|
-
|
|
|
- let params = json!([
|
|
|
- query,
|
|
|
- QueryType::Integer as u8,
|
|
|
- DAO_DAOS_COL_DAO_ID,
|
|
|
- QueryType::Blob as u8,
|
|
|
- DAO_DAOS_COL_NAME,
|
|
|
- QueryType::Integer as u8,
|
|
|
- DAO_DAOS_COL_PROPOSER_LIMIT,
|
|
|
- QueryType::Integer as u8,
|
|
|
- DAO_DAOS_COL_QUORUM,
|
|
|
- QueryType::Integer as u8,
|
|
|
- DAO_DAOS_COL_APPROVAL_RATIO_BASE,
|
|
|
- QueryType::Integer as u8,
|
|
|
- DAO_DAOS_COL_APPROVAL_RATIO_QUOT,
|
|
|
- QueryType::Blob as u8,
|
|
|
- DAO_DAOS_COL_GOV_TOKEN_ID,
|
|
|
- QueryType::Blob as u8,
|
|
|
- DAO_DAOS_COL_SECRET,
|
|
|
+ MONEY_COINS_COL_VALUE,
|
|
|
QueryType::Blob as u8,
|
|
|
- DAO_DAOS_COL_BULLA_BLIND,
|
|
|
- QueryType::OptionBlob as u8,
|
|
|
- DAO_DAOS_COL_LEAF_POSITION,
|
|
|
- QueryType::OptionBlob as u8,
|
|
|
- DAO_DAOS_COL_TX_HASH,
|
|
|
- QueryType::OptionInteger as u8,
|
|
|
- DAO_DAOS_COL_CALL_INDEX,
|
|
|
+ MONEY_COINS_COL_TOKEN_ID,
|
|
|
]);
|
|
|
|
|
|
let req = JsonRequest::new("wallet.query_row_multi", params);
|
|
|
let rep = self.rpc_client.request(req).await?;
|
|
|
|
|
|
+ // The returned thing should be an array of found rows.
|
|
|
let Some(rows) = rep.as_array() else {
|
|
|
- return Err(anyhow!("Unexpected response from darkfid: {}", rep));
|
|
|
+ return Err(anyhow!("[money_balance] Unexpected response from darkfid: {}", rep))
|
|
|
};
|
|
|
|
|
|
- let mut daos = Vec::with_capacity(rows.len());
|
|
|
+ // Fill this map with balances
|
|
|
+ let mut balmap: HashMap<String, u64> = HashMap::new();
|
|
|
|
|
|
+ // Let's scan through the rows and see if we got anything.
|
|
|
+ // TODO: Separate tokens with spend_hook != 0
|
|
|
for row in rows {
|
|
|
- let id: u64 = serde_json::from_value(row[0].clone())?;
|
|
|
-
|
|
|
- let name_bytes: Vec<u8> = serde_json::from_value(row[1].clone())?;
|
|
|
- let name = deserialize(&name_bytes)?;
|
|
|
+ let Some(row) = row.as_array() else {
|
|
|
+ return Err(anyhow!("[money_balance] Unexpected response from darkfid: {}", rep))
|
|
|
+ };
|
|
|
|
|
|
- let proposer_limit = serde_json::from_value(row[2].clone())?;
|
|
|
- let quorum = serde_json::from_value(row[3].clone())?;
|
|
|
- let approval_ratio_base = serde_json::from_value(row[4].clone())?;
|
|
|
- let approval_ratio_quot = serde_json::from_value(row[5].clone())?;
|
|
|
+ if row.len() != 2 {
|
|
|
+ eprintln!("Error: Got invalid array, row should contain two elements.");
|
|
|
+ eprintln!("Actual contents:\n:{:#?}", row);
|
|
|
+ return Err(anyhow!("[money_balance] Unexpected response from darkfid: {}", rep))
|
|
|
+ }
|
|
|
|
|
|
- let gov_token_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
|
|
|
- let gov_token_id = deserialize(&gov_token_bytes)?;
|
|
|
+ let value_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
|
|
|
+ let mut value: u64 = deserialize(&value_bytes)?;
|
|
|
|
|
|
- let secret_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
|
|
|
- let secret_key = deserialize(&secret_bytes)?;
|
|
|
+ let token_bytes: Vec<u8> = serde_json::from_value(row[1].clone())?;
|
|
|
+ let token_id: TokenId = deserialize(&token_bytes)?;
|
|
|
+ let token_id = format!("{}", token_id);
|
|
|
|
|
|
- let bulla_blind_bytes: Vec<u8> = serde_json::from_value(row[8].clone())?;
|
|
|
- let bulla_blind = deserialize(&bulla_blind_bytes)?;
|
|
|
+ if let Some(prev) = balmap.get(&token_id) {
|
|
|
+ value += prev;
|
|
|
+ }
|
|
|
|
|
|
- let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[9].clone())?;
|
|
|
- let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[10].clone())?;
|
|
|
- let call_index = serde_json::from_value(row[11].clone())?;
|
|
|
+ balmap.insert(token_id, value);
|
|
|
+ }
|
|
|
|
|
|
- let leaf_position = if leaf_position_bytes.is_empty() {
|
|
|
- None
|
|
|
- } else {
|
|
|
- Some(deserialize(&leaf_position_bytes)?)
|
|
|
- };
|
|
|
+ Ok(balmap)
|
|
|
+ }
|
|
|
|
|
|
- let tx_hash =
|
|
|
- if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
|
|
|
-
|
|
|
- let dao = Dao {
|
|
|
- id,
|
|
|
- name,
|
|
|
- proposer_limit,
|
|
|
- quorum,
|
|
|
- approval_ratio_base,
|
|
|
- approval_ratio_quot,
|
|
|
- gov_token_id,
|
|
|
- secret_key,
|
|
|
- bulla_blind,
|
|
|
- leaf_position,
|
|
|
- tx_hash,
|
|
|
- call_index,
|
|
|
- };
|
|
|
+ /// Get the last scanned slot from the wallet
|
|
|
+ pub async fn last_scanned_slot(&self) -> Result<u64> {
|
|
|
+ let query =
|
|
|
+ format!("SELECT {} FROM {};", MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE);
|
|
|
|
|
|
- daos.push(dao);
|
|
|
- }
|
|
|
+ let params = json!([query, QueryType::Integer as u8, MONEY_INFO_COL_LAST_SCANNED_SLOT]);
|
|
|
+ let req = JsonRequest::new("wallet.query_row_single", params);
|
|
|
+ let rep = self.rpc_client.request(req).await?;
|
|
|
|
|
|
- // Sort by ID in SQL. The SELECT statement does not guarantee this.
|
|
|
- daos.sort_by(|a, b| a.id.cmp(&b.id));
|
|
|
- Ok(daos)
|
|
|
+ Ok(serde_json::from_value(rep[0].clone())?)
|
|
|
}
|
|
|
}
|