Răsfoiți Sursa

darkfid/wallet: Implement arbitrary wallet queries over JSON-RPC.

parazyd 3 ani în urmă
părinte
comite
10e8d8e5fe
4 a modificat fișierele cu 107 adăugiri și 304 ștergeri
  1. 1 0
      bin/darkfid/Cargo.toml
  2. 14 5
      bin/darkfid/src/main.rs
  3. 71 299
      bin/darkfid/src/rpc_wallet.rs
  4. 21 0
      src/wallet/walletdb.rs

+ 1 - 0
bin/darkfid/Cargo.toml

@@ -29,6 +29,7 @@ serde_json = "1.0.87"
 simplelog = "0.12.0"
 simplelog = "0.12.0"
 sled = "0.34.7"
 sled = "0.34.7"
 smol = "1.2.5"
 smol = "1.2.5"
+sqlx = {version = "0.6.2", features = ["runtime-async-std-native-tls", "sqlite"]}
 url = "2.3.1"
 url = "2.3.1"
 
 
 # Argument parsing
 # Argument parsing

+ 14 - 5
bin/darkfid/src/main.rs

@@ -48,7 +48,7 @@ use darkfi::{
         server::{listen_and_serve, RequestHandler},
         server::{listen_and_serve, RequestHandler},
     },
     },
     util::path::expand_path,
     util::path::expand_path,
-    wallet::walletdb::init_wallet,
+    wallet::{walletdb::init_wallet, WalletPtr},
     Error, Result,
     Error, Result,
 };
 };
 
 
@@ -175,6 +175,7 @@ pub struct Darkfid {
     synced: Mutex<bool>, // AtomicBool is weird in Arc
     synced: Mutex<bool>, // AtomicBool is weird in Arc
     _consensus_p2p: Option<P2pPtr>,
     _consensus_p2p: Option<P2pPtr>,
     sync_p2p: Option<P2pPtr>,
     sync_p2p: Option<P2pPtr>,
+    wallet: WalletPtr,
     validator_state: ValidatorStatePtr,
     validator_state: ValidatorStatePtr,
 }
 }
 
 
@@ -182,7 +183,7 @@ pub struct Darkfid {
 mod rpc_blockchain;
 mod rpc_blockchain;
 mod rpc_misc;
 mod rpc_misc;
 //mod rpc_tx;
 //mod rpc_tx;
-//mod rpc_wallet;
+mod rpc_wallet;
 
 
 // Internal methods
 // Internal methods
 //mod internal;
 //mod internal;
@@ -256,8 +257,15 @@ impl Darkfid {
         validator_state: ValidatorStatePtr,
         validator_state: ValidatorStatePtr,
         consensus_p2p: Option<P2pPtr>,
         consensus_p2p: Option<P2pPtr>,
         sync_p2p: Option<P2pPtr>,
         sync_p2p: Option<P2pPtr>,
+        wallet: WalletPtr,
     ) -> Self {
     ) -> Self {
-        Self { synced: Mutex::new(false), _consensus_p2p: consensus_p2p, sync_p2p, validator_state }
+        Self {
+            synced: Mutex::new(false),
+            _consensus_p2p: consensus_p2p,
+            sync_p2p,
+            wallet,
+            validator_state,
+        }
     }
     }
 }
 }
 
 
@@ -329,7 +337,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'_>>) -> Result<()> {
         &sled_db,
         &sled_db,
         genesis_ts,
         genesis_ts,
         genesis_data,
         genesis_data,
-        wallet,
+        wallet.clone(),
         cashier_pubkeys,
         cashier_pubkeys,
         faucet_pubkeys,
         faucet_pubkeys,
     )
     )
@@ -418,7 +426,8 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'_>>) -> Result<()> {
     };
     };
 
 
     // Initialize program state
     // Initialize program state
-    let darkfid = Darkfid::new(state.clone(), consensus_p2p.clone(), sync_p2p.clone()).await;
+    let darkfid =
+        Darkfid::new(state.clone(), consensus_p2p.clone(), sync_p2p.clone(), wallet.clone()).await;
     let darkfid = Arc::new(darkfid);
     let darkfid = Arc::new(darkfid);
 
 
     // JSON-RPC server
     // JSON-RPC server

+ 71 - 299
bin/darkfid/src/rpc_wallet.rs

@@ -16,19 +16,17 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use darkfi_sdk::crypto::{Address, Keypair, PublicKey, SecretKey, TokenId};
-use darkfi_serial::{deserialize, serialize};
-use fxhash::FxHashMap;
-use incrementalmerkletree::Tree;
+use darkfi_sdk::crypto::{Keypair, PublicKey, SecretKey, TokenId};
 use log::error;
 use log::error;
 use serde_json::{json, Value};
 use serde_json::{json, Value};
+use sqlx::Row;
 
 
 use darkfi::{
 use darkfi::{
-    node::State,
     rpc::jsonrpc::{
     rpc::jsonrpc::{
         ErrorCode::{InternalError, InvalidParams, ParseError},
         ErrorCode::{InternalError, InvalidParams, ParseError},
         JsonError, JsonResponse, JsonResult,
         JsonError, JsonResponse, JsonResult,
     },
     },
+    wallet::walletdb::QueryType,
 };
 };
 
 
 use super::Darkfid;
 use super::Darkfid;
@@ -36,327 +34,101 @@ use crate::{server_error, RpcError};
 
 
 impl Darkfid {
 impl Darkfid {
     // RPCAPI:
     // RPCAPI:
-    // Attempts to generate a new keypair and returns its address upon success.
+    // Attempts to query for a single row in a given table.
+    // The parameters given contain paired metadata so we know how to decode the SQL data.
+    // An example of `params` is as such:
+    // ```
+    // params[0] -> "sql query"
+    // params[1] -> column_type
+    // params[2] -> "column_name"
+    // ...
+    // params[n-1] -> column_type
+    // params[n] -> "column_name"
+    // ```
+    // This function will fetch the first row it finds, if any. The `column_type` field
+    // is a type available in the `WalletDb` API as an enum called `QueryType`. If a row
+    // is not found, the returned result will be a JSON-RPC error.
+    // NOTE: This is obviously vulnerable to SQL injection. Open to interesting solutions.
     //
     //
-    // --> {"jsonrpc": "2.0", "method": "wallet.keygen", "params": [], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "1DarkFi...", "id": 1}
-    pub async fn wallet_keygen(&self, id: Value, params: &[Value]) -> JsonResult {
-        if !params.is_empty() {
+    // --> {"jsonrpc": "2.0", "method": "wallet.query_row_single", "params": [...], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": ["va", "lu", "es", ...], "id": 1}
+    pub async fn wallet_query_row_single(&self, id: Value, params: &[Value]) -> JsonResult {
+        // TODO: Better errors
+
+        // We need at least 3 params for something we want to fetch, and we want them in pairs.
+        // Also the first param should be a String
+        if params.len() < 3 || params[1..].len() % 2 != 0 || !params[0].is_string() {
             return JsonError::new(InvalidParams, None, id).into()
             return JsonError::new(InvalidParams, None, id).into()
         }
         }
 
 
-        match self.client.keygen().await {
-            Ok(a) => JsonResponse::new(json!(a.to_string()), id).into(),
-            Err(e) => {
-                error!("[RPC] wallet.keygen: Failed creating keypair: {}", e);
-                server_error(RpcError::Keygen, id, None)
-            }
-        }
-    }
-
-    // RPCAPI:
-    // Fetches public keys by given indexes from the wallet and returns it in an
-    // encoded format. `-1` is supported to fetch all available keys.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.get_addrs", "params": [1, 2], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": ["foo", "bar"], "id": 1}
-    pub async fn wallet_get_addrs(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let mut fetch_all = false;
-
-        for (i, elem) in params.iter().enumerate() {
-            if !elem.is_i64() {
-                error!("[RPC] wallet.get_addrs: Param {} is not i64", i);
-                return server_error(RpcError::NaN, id, Some(&format!("Param {} is not i64", i)))
-            }
-
-            if elem.as_i64() == Some(-1) {
-                if params.len() != 1 {
-                    return server_error(
-                        RpcError::ParseError,
-                        id,
-                        Some("-1 can only be used as a single param"),
-                    )
-                }
-
-                fetch_all = true;
-                break
-            }
-
-            if elem.as_i64() < Some(-1) {
-                return server_error(RpcError::LessThanNegOne, id, None)
-            }
-        }
-
-        let keypairs = match self.client.get_keypairs().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.get_addrs: Failed fetching keypairs: {}", e);
-                return server_error(RpcError::KeypairFetch, id, None)
-            }
-        };
-
-        if fetch_all {
-            let ret: Vec<String> =
-                keypairs.iter().map(|x| Address::from(x.public).to_string()).collect();
-            return JsonResponse::new(json!(ret), id).into()
-        }
-
-        let mut ret = vec![];
-        for i in params {
-            // This cast is safe on 64bit since we've already sorted out
-            // all negative cases above.
-            let idx = i.as_i64().unwrap() as usize;
-            if let Some(kp) = keypairs.get(idx) {
-                ret.push(Some(Address::from(kp.public).to_string()));
-            } else {
-                ret.push(None)
-            }
-        }
-
-        JsonResponse::new(json!(ret), id).into()
-    }
-
-    // RPCAPI:
-    // Exports the given keypair index.
-    // Returns the encoded secret key upon success.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.export_keypair", "params": [0], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "foobar", "id": 1}
-    pub async fn wallet_export_keypair(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 1 || !params[0].is_u64() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let keypairs = match self.client.get_keypairs().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.export_keypair: Failed fetching keypairs: {}", e);
-                return server_error(RpcError::KeypairFetch, id, None)
-            }
-        };
-
-        if let Some(kp) = keypairs.get(params[0].as_u64().unwrap() as usize) {
-            return JsonResponse::new(json!(serialize(&kp.secret)), id).into()
-        }
-
-        server_error(RpcError::KeypairNotFound, id, None)
-    }
-
-    // RPCAPI:
-    // Imports a given secret key into the wallet as a keypair.
-    // Returns the public counterpart as the result upon success.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.import_keypair", "params": ["foobar"], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "pubfoobar", "id": 1}
-    pub async fn wallet_import_keypair(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let bytes: [u8; 32] = match serde_json::from_str(params[0].as_str().unwrap()) {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.import_keypair: Failed parsing secret key from string: {}", e);
-                return server_error(RpcError::InvalidKeypair, id, None)
+        // The remaining pairs should be typed properly too
+        let mut types: Vec<QueryType> = vec![];
+        let mut names: Vec<&str> = vec![];
+        for pair in params[1..].chunks(2) {
+            if !pair[0].is_u64() || !pair[1].is_string() {
+                return JsonError::new(InvalidParams, None, id).into()
             }
             }
-        };
 
 
-        let secret = match SecretKey::from_bytes(bytes) {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.import_keypair: Failed parsing secret key from string: {}", e);
-                return server_error(RpcError::InvalidKeypair, id, None)
+            let typ = pair[0].as_u64().unwrap();
+            if typ >= QueryType::Last as u64 {
+                return JsonError::new(InvalidParams, None, id).into()
             }
             }
-        };
 
 
-        let public = PublicKey::from_secret(secret);
-        let keypair = Keypair { secret, public };
-        let address = Address::from(public).to_string();
-
-        if let Err(e) = self.client.put_keypair(&keypair).await {
-            error!("[RPC] wallet.import_keypair: Failed inserting keypair into wallet: {}", e);
-            return JsonError::new(InternalError, None, id).into()
+            types.push((typ as u8).into());
+            names.push(pair[1].as_str().unwrap());
         }
         }
 
 
-        JsonResponse::new(json!(address), id).into()
-    }
-
-    // RPCAPI:
-    // Sets the default wallet address to the given index.
-    // Returns `true` upon success.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.set_default_address", "params": [2], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
-    pub async fn wallet_set_default_address(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 1 || !params[0].is_u64() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let idx = params[0].as_u64().unwrap();
-
-        let keypairs = match self.client.get_keypairs().await {
+        // Get a wallet connection
+        let mut conn = match self.wallet.conn.acquire().await {
             Ok(v) => v,
             Ok(v) => v,
             Err(e) => {
             Err(e) => {
-                error!("[RPC] wallet.set_default_address: Failed fetching keypairs: {}", e);
-                return server_error(RpcError::KeypairFetch, id, None)
-            }
-        };
-
-        if keypairs.len() as u64 != idx - 1 {
-            return server_error(RpcError::KeypairNotFound, id, None)
-        }
-
-        let kp = keypairs[idx as usize];
-
-        if let Err(e) = self.client.set_default_keypair(&kp.public).await {
-            error!("[RPC] wallet.set_default_address: Failed setting default keypair: {}", e);
-            return JsonError::new(InternalError, None, id).into()
-        }
-
-        JsonResponse::new(json!(true), id).into()
-    }
-
-    // RPCAPI:
-    // Queries the wallet for known tokens with active balances.
-    // Returns a map of balances, indexed by the token ID.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.get_balances", "params": [], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": [{"1Foobar...": 100}, {...}]", "id": 1}
-    pub async fn wallet_get_balances(&self, id: Value, _params: &[Value]) -> JsonResult {
-        let balances = match self.client.get_balances().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.get_balances: Failed fetching balances from wallet: {}", e);
+                error!("[RPC] wallet.query_row_single: Failed to acquire wallet connection: {}", e);
                 return JsonError::new(InternalError, None, id).into()
                 return JsonError::new(InternalError, None, id).into()
             }
             }
         };
         };
 
 
-        // k: token_id, v: [amount]
-        let mut ret: FxHashMap<String, u64> = FxHashMap::default();
-
-        for balance in balances.list {
-            let token_id = format!("{}", TokenId::from(balance.token_id));
-            let mut amount = balance.value;
-
-            if let Some(prev) = ret.get(&token_id) {
-                amount += prev;
-            }
-
-            ret.insert(token_id, amount);
-        }
-
-        JsonResponse::new(json!(ret), id).into()
-    }
-
-    // RPCAPI:
-    // Queries the wallet for a coin containing given parameters (value, token_id, unspent),
-    // and returns the entire row with the coin's data:
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.get_coins_valtok", "params": [1234, "F00b4r...", true], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": ["coin", "data", ...], "id": 1}
-    pub async fn wallet_get_coins_valtok(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 3 ||
-            !params[0].is_u64() ||
-            !params[1].is_string() ||
-            !params[2].is_boolean()
-        {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let value = params[0].as_u64().unwrap();
-        let unspent = params[2].as_bool().unwrap();
-        let token_id = match TokenId::try_from(params[1].as_str().unwrap()) {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.get_coins_valtok: Failed parsing token_id from base58: {}", e);
-                return JsonError::new(ParseError, None, id).into()
-            }
-        };
-
-        let coins = match self.client.get_coins_valtok(value, token_id, unspent).await {
+        // Execute the query and see if we find a row
+        let row = match sqlx::query(params[0].as_str().unwrap()).fetch_one(&mut conn).await {
             Ok(v) => v,
             Ok(v) => v,
             Err(e) => {
             Err(e) => {
-                error!("[RPC] wallet.get_coins_valtok: Failed fetching from wallet: {}", e);
+                error!("[RPC] wallet.query_row_single: Failed to execute SQL query: {}", e);
                 return JsonError::new(InternalError, None, id).into()
                 return JsonError::new(InternalError, None, id).into()
             }
             }
         };
         };
 
 
-        let ret: Vec<String> =
-            coins.iter().map(|x| bs58::encode(serialize(x)).into_string()).collect();
-        JsonResponse::new(json!(ret), id).into()
-    }
-
-    // RPCAPI:
-    // Query the state merkle tree for the merkle path of a given leaf position.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.get_merkle_path", "params": [3], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": ["f091uf1...", "081ff0h10w1h0...", ...], "id": 1}
-    pub async fn wallet_get_merkle_path(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 1 || !params[0].is_u64() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let leaf_pos: incrementalmerkletree::Position =
-            ((params[0].as_u64().unwrap() as u64) as usize).into();
-
-        let validator_state = self.validator_state.read().await;
-        let state = validator_state.state_machine.lock().await;
-        let root = state.tree.root(0).unwrap();
-        let merkle_path = state.tree.authentication_path(leaf_pos, &root).unwrap();
-        drop(state);
-        drop(validator_state);
-
-        let ret: Vec<String> =
-            merkle_path.iter().map(|x| bs58::encode(serialize(x)).into_string()).collect();
-        JsonResponse::new(json!(ret), id).into()
-    }
-
-    // RPCAPI:
-    // Try to decrypt a given encrypted note with the secret keys
-    // found in the wallet.
-    //
-    // --> {"jsonrpc": "2.0", "method": "wallet.decrypt_note", params": [ciphertext], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "base58_encoded_plain_note", "id": 1}
-    pub async fn wallet_decrypt_note(&self, id: Value, params: &[Value]) -> JsonResult {
-        if params.len() != 1 || !params[0].is_string() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let bytes = match bs58::decode(params[0].as_str().unwrap()).into_vec() {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.decrypt_note: Failed decoding base58 string: {}", e);
-                return JsonError::new(ParseError, None, id).into()
-            }
-        };
+        // Try to decode the row into what was requested
+        let mut ret: Vec<Value> = vec![];
+
+        for (typ, col) in types.iter().zip(names) {
+            match typ {
+                QueryType::Integer => {
+                    let value: i32 = match row.try_get(col) {
+                        Ok(v) => v,
+                        Err(e) => {
+                            error!("[RPC] wallet.query_row_single: {}", e);
+                            return JsonError::new(InternalError, None, id).into()
+                        }
+                    };
+
+                    ret.push(json!(value));
+                }
 
 
-        let enc_note = match deserialize(&bytes) {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.decrypt_note: Failed deserializing into EncryptedNote: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+                QueryType::Blob => {
+                    let value: Vec<u8> = match row.try_get(col) {
+                        Ok(v) => v,
+                        Err(e) => {
+                            error!("[RPC] wallet.query_row_single: {}", e);
+                            return JsonError::new(InternalError, None, id).into()
+                        }
+                    };
 
 
-        let keypairs = match self.client.get_keypairs().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!("[RPC] wallet.decrypt_note: Failed fetching keypairs: {}", e);
-                return JsonError::new(InternalError, None, id).into()
-            }
-        };
+                    ret.push(json!(value));
+                }
 
 
-        for kp in keypairs {
-            if let Some(note) = State::try_decrypt_note(&enc_note, kp.secret) {
-                let s = bs58::encode(&serialize(&note)).into_string();
-                return JsonResponse::new(json!(s), id).into()
+                _ => unreachable!(),
             }
             }
         }
         }
 
 
-        server_error(RpcError::DecryptionFailed, id, None)
+        JsonResponse::new(json!(ret), id).into()
     }
     }
 }
 }

+ 21 - 0
src/wallet/walletdb.rs

@@ -37,6 +37,27 @@ pub async fn init_wallet(wallet_path: &str, wallet_pass: &str) -> Result<WalletP
     Ok(wallet)
     Ok(wallet)
 }
 }
 
 
+/// Types we want to allow to query from the SQL wallet
+#[repr(u8)]
+pub enum QueryType {
+    /// Integer gets decoded into u64,
+    Integer = 0x00,
+    /// Blob gets decoded into Vec<u8>,
+    Blob = 0x01,
+    /// Last type, increment this when you add new types.
+    Last = 0x02,
+}
+
+impl From<u8> for QueryType {
+    fn from(x: u8) -> Self {
+        match x {
+            0x00 => Self::Integer,
+            0x01 => Self::Blob,
+            _ => unimplemented!(),
+        }
+    }
+}
+
 /// Structure representing base wallet operations.
 /// Structure representing base wallet operations.
 /// Additional operations can be implemented by trait extensions.
 /// Additional operations can be implemented by trait extensions.
 pub struct WalletDb {
 pub struct WalletDb {