Kaynağa Gözat

drk: drk rewritte skeleton added

aggstam 2 yıl önce
ebeveyn
işleme
4c8dab8204

+ 1 - 0
Cargo.toml

@@ -24,6 +24,7 @@ members = [
     "bin/darkfid2",
     "bin/darkfi-mmproxy",
     #"bin/drk",
+    #"bin/drk2",
     #"bin/faucetd",
     #"bin/fud/fu",
     #"bin/fud/fud",

+ 1 - 1
bin/darkfid2/src/rpc.rs

@@ -146,7 +146,7 @@ impl Darkfid {
     }
 
     // RPCAPI:
-    // Pings configured miner daemon for livenes.
+    // Pings configured miner daemon for liveness.
     // Returns `true` on success.
     //
     // --> {"jsonrpc": "2.0", "method": "ping_miner", "params": [], "id": 1}

+ 31 - 0
bin/drk2/Cargo.toml

@@ -0,0 +1,31 @@
+[package]
+name = "drk2"
+version = "0.4.1"
+homepage = "https://dark.fi"
+description = "Command-line client for darkfid"
+authors = ["Dyne.org foundation <foundation@dyne.org>"]
+repository = "https://github.com/darkrenaissance/darkfi"
+license = "AGPL-3.0-only"
+edition = "2021"
+
+[dependencies]
+# Darkfi
+darkfi = {path = "../../", features = ["async-daemonize", "rpc"]}
+
+# Misc
+log = "0.4.20"
+rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]}
+rusqlite = {version = "0.30.0", features = ["sqlcipher"]}
+url = "2.5.0"
+
+# Daemon
+easy-parallel = "3.3.1"
+signal-hook-async-std = "0.2.2"
+signal-hook = "0.3.17"
+simplelog = "0.12.1"
+smol = "1.3.0"
+
+# Argument parsing
+serde = {version = "1.0.195", features = ["derive"]}
+structopt = "0.3.26"
+structopt-toml = "0.5.1"

+ 42 - 0
bin/drk2/Makefile

@@ -0,0 +1,42 @@
+.POSIX:
+
+# Install prefix
+PREFIX = $(HOME)/.cargo
+
+# Cargo binary
+CARGO = cargo +nightly
+
+# Compile target
+RUST_TARGET = $(shell rustc -Vv | grep '^host: ' | cut -d' ' -f2)
+# Uncomment when doing musl static builds
+#RUSTFLAGS = -C target-feature=+crt-static -C link-self-contained=yes
+
+SRC = \
+	Cargo.toml \
+	../../Cargo.toml \
+	$(shell find src -type f -name '*.rs') \
+	$(shell find ../../src -type f -name '*.rs') \
+	$(shell find ../../src/contract -type f -name '*.wasm')
+
+BIN = $(shell grep '^name = ' Cargo.toml | cut -d' ' -f3 | tr -d '"')
+
+all: $(BIN)
+
+$(BIN): $(SRC)
+	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) build --target=$(RUST_TARGET) --release --package $@
+	cp -f ../../target/$(RUST_TARGET)/release/$@ $@
+	cp -f ../../target/$(RUST_TARGET)/release/$@ ../../$@
+
+clean:
+	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) clean --target=$(RUST_TARGET) --release --package $(BIN)
+	rm -f $(BIN) ../../$(BIN)
+
+install: all
+	mkdir -p $(DESTDIR)$(PREFIX)/bin
+	cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
+	chmod 755 $(DESTDIR)$(PREFIX)/bin/$(BIN)
+
+uninstall:
+	rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN)
+
+.PHONY: all clean install uninstall

+ 16 - 0
bin/drk2/drk_config.toml

@@ -0,0 +1,16 @@
+## darkfid configuration file
+##
+## Please make sure you go through all the settings so you can configure
+## your daemon properly.
+##
+## The default values are left commented. They can be overridden either by
+## uncommenting, or by using the command-line.
+
+# Path to wallet database
+wallet_path = "~/.local/darkfi/drk/wallet.db"
+
+# Password for the wallet database
+wallet_pass = "changeme"
+
+# darkfid JSON-RPC endpoint
+endpoint = "tcp://127.0.0.1:8340"

+ 39 - 0
bin/drk2/src/cli_util.rs

@@ -0,0 +1,39 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 <https://www.gnu.org/licenses/>.
+ */
+use std::io::Cursor;
+
+use rodio::{source::Source, Decoder, OutputStream};
+
+use darkfi::system::sleep;
+
+/// Fun police go away
+pub async fn kaching() {
+    const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3");
+
+    let cursor = Cursor::new(WALLET_MP3);
+
+    let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return };
+
+    let Ok(source) = Decoder::new(cursor) else { return };
+
+    if stream_handle.play_raw(source.convert_samples()).is_err() {
+        return
+    }
+
+    sleep(2).await;
+}

+ 37 - 0
bin/drk2/src/error.rs

@@ -0,0 +1,37 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+/// Result type used in the wallet database module
+pub type WalletDbResult<T> = std::result::Result<T, WalletDbError>;
+
+/// Custom wallet database errors available for drk.
+/// Please sort them sensefully.
+#[derive(Debug)]
+pub enum WalletDbError {
+    // Connection related errors
+    ConnectionFailed = -32100,
+
+    // Configuration related errors
+    PragmaUpdateError = -32110,
+
+    // Query execution related errors
+    QueryPreparationFailed = -32120,
+    QueryExecutionFailed = -32121,
+    QueryFinalizationFailed = -32122,
+    ParseColumnValueError = -32123,
+}

+ 205 - 0
bin/drk2/src/main.rs

@@ -0,0 +1,205 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+use std::{process::exit, sync::Arc, time::Instant};
+
+use smol::stream::StreamExt;
+use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
+use url::Url;
+
+use darkfi::{
+    async_daemonize, cli_desc,
+    rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue},
+    util::path::expand_path,
+    Result,
+};
+
+/// Error codes
+mod error;
+
+/// CLI utility functions
+mod cli_util;
+use cli_util::kaching;
+
+/// Wallet database operations handler
+mod walletdb;
+use walletdb::{WalletDb, WalletPtr};
+
+const CONFIG_FILE: &str = "drk_config.toml";
+const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml");
+
+#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
+#[serde(default)]
+#[structopt(name = "drk", about = cli_desc!())]
+struct Args {
+    #[structopt(short, long)]
+    /// Configuration file to use
+    config: Option<String>,
+
+    #[structopt(long, default_value = "~/.local/darkfi/drk/wallet.db")]
+    /// Path to wallet database
+    wallet_path: String,
+
+    #[structopt(long, default_value = "changeme")]
+    /// Password for the wallet database
+    wallet_pass: String,
+
+    #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
+    /// darkfid JSON-RPC endpoint
+    endpoint: Url,
+
+    #[structopt(subcommand)]
+    /// Sub command to execute
+    command: Subcmd,
+
+    #[structopt(short, long)]
+    /// Set log file to ouput into
+    log: Option<String>,
+
+    #[structopt(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
+}
+
+#[derive(Clone, Debug, Deserialize, StructOpt)]
+enum Subcmd {
+    /// Fun
+    Kaching,
+
+    /// Send a ping request to the darkfid RPC endpoint
+    Ping,
+
+    // TODO: shell completions
+    /// Wallet operations
+    Wallet {
+        #[structopt(long)]
+        /// Initialize wallet database
+        initialize: bool,
+
+        #[structopt(long)]
+        /// Generate a new keypair in the wallet
+        keygen: bool,
+
+        #[structopt(long)]
+        /// Query the wallet for known balances
+        balance: bool,
+
+        #[structopt(long)]
+        /// Get the default address in the wallet
+        address: bool,
+
+        #[structopt(long)]
+        /// Print all the secret keys from the wallet
+        secrets: bool,
+
+        #[structopt(long)]
+        /// Import secret keys from stdin into the wallet, separated by newlines
+        import_secrets: bool,
+
+        #[structopt(long)]
+        /// Print the Merkle tree in the wallet
+        tree: bool,
+
+        #[structopt(long)]
+        /// Print all the coins in the wallet
+        coins: bool,
+    },
+}
+
+/// CLI-util structure
+pub struct Drk {
+    /// Wallet database operations handler
+    pub wallet: WalletPtr,
+    /// JSON-RPC client to execute requests to darkfid daemon
+    pub rpc_client: RpcClient,
+}
+
+impl Drk {
+    async fn new(
+        wallet_path: String,
+        wallet_pass: String,
+        endpoint: Url,
+        ex: Arc<smol::Executor<'static>>,
+    ) -> Result<Self> {
+        let wallet = match WalletDb::new(Some(expand_path(&wallet_path)?), Some(&wallet_pass)) {
+            Ok(w) => w,
+            Err(e) => {
+                eprintln!("Error initializing wallet: {e:?}");
+                exit(2);
+            }
+        };
+        let rpc_client = RpcClient::new(endpoint, ex).await?;
+        Ok(Self { wallet, rpc_client })
+    }
+
+    /// Auxilliary function to ping configured darkfid daemon for liveness.
+    async fn ping(&self) -> Result<()> {
+        eprintln!("Executing ping request to darkfid...");
+        let latency = Instant::now();
+        let req = JsonRequest::new("ping", JsonValue::Array(vec![]));
+        let rep = self.rpc_client.oneshot_request(req).await?;
+        let latency = latency.elapsed();
+        eprintln!("Got reply: {:?}", rep);
+        eprintln!("Latency: {:?}", latency);
+        Ok(())
+    }
+}
+
+async_daemonize!(realmain);
+async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
+    match args.command {
+        Subcmd::Kaching => {
+            kaching().await;
+            Ok(())
+        }
+
+        Subcmd::Ping => {
+            let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?;
+            drk.ping().await
+        }
+
+        Subcmd::Wallet {
+            initialize,
+            keygen,
+            balance,
+            address,
+            secrets,
+            import_secrets,
+            tree,
+            coins,
+        } => {
+            if !initialize &&
+                !keygen &&
+                !balance &&
+                !address &&
+                !secrets &&
+                !tree &&
+                !coins &&
+                !import_secrets
+            {
+                eprintln!("Error: You must use at least one flag for this subcommand");
+                eprintln!("Run with \"wallet -h\" to see the subcommand usage.");
+                exit(2);
+            }
+
+            // TODO
+
+            Ok(())
+        }
+    }
+}

+ 213 - 0
bin/drk2/src/walletdb.rs

@@ -0,0 +1,213 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+use std::{path::PathBuf, sync::Arc};
+
+use log::{debug, error, info};
+use rusqlite::{
+    types::{ToSql, Value},
+    Connection,
+};
+use smol::lock::Mutex;
+
+use crate::error::{WalletDbError, WalletDbResult};
+
+pub type WalletPtr = Arc<WalletDb>;
+
+/// Structure representing base wallet database operations.
+pub struct WalletDb {
+    /// Connection to the SQLite database
+    pub conn: Mutex<Connection>,
+}
+
+impl WalletDb {
+    /// Create a new wallet database handler. If `path` is `None`, create it in memory.
+    pub fn new(path: Option<PathBuf>, password: Option<&str>) -> WalletDbResult<WalletPtr> {
+        let Ok(conn) = (match path.clone() {
+            Some(p) => Connection::open(p),
+            None => Connection::open_in_memory(),
+        }) else {
+            return Err(WalletDbError::ConnectionFailed);
+        };
+
+        if let Some(password) = password {
+            if let Err(e) = conn.pragma_update(None, "key", password) {
+                error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
+                return Err(WalletDbError::PragmaUpdateError);
+            };
+        }
+        if let Err(e) = conn.pragma_update(None, "foreign_keys", "ON") {
+            error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
+            return Err(WalletDbError::PragmaUpdateError);
+        };
+
+        info!(target: "walletdb::new", "[WalletDb] Opened Sqlite connection at \"{path:?}\"");
+        Ok(Arc::new(Self { conn: Mutex::new(conn) }))
+    }
+
+    /// This function executes a given SQL query, but isn't able to return anything.
+    /// Therefore it's best to use it for initializing a table or similar things.
+    pub async fn exec_sql(&self, query: &str, params: &[&dyn ToSql]) -> WalletDbResult<()> {
+        debug!(target: "walletdb::exec_sql", "[WalletDb] Executing SQL query:\n{query}");
+        // If no params are provided, execute directly
+        if params.is_empty() {
+            if let Err(e) = self.conn.lock().await.execute(query, ()) {
+                error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
+                return Err(WalletDbError::QueryExecutionFailed)
+            };
+            return Ok(())
+        }
+
+        // First we prepare the query
+        let conn = self.conn.lock().await;
+        let Ok(mut stmt) = conn.prepare(query) else {
+            return Err(WalletDbError::QueryPreparationFailed)
+        };
+
+        // Execute the query using provided params
+        if let Err(e) = stmt.execute(params) {
+            error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
+            return Err(WalletDbError::QueryExecutionFailed)
+        };
+
+        // Finalize query and drop connection lock
+        if let Err(e) = stmt.finalize() {
+            error!(target: "walletdb::exec_sql", "[WalletDb] Query finalization failed: {e}");
+            return Err(WalletDbError::QueryFinalizationFailed)
+        };
+        drop(conn);
+
+        Ok(())
+    }
+
+    /// Query provided table from selected column names and provided `WHERE` clauses.
+    /// Named parameters are supported in the `WHERE` clauses, assuming they follow the
+    /// normal formatting ":{column_name}"
+    pub async fn query_single(
+        &self,
+        table: &str,
+        col_names: Vec<&str>,
+        params: &[(&str, &dyn ToSql)],
+    ) -> WalletDbResult<Vec<Value>> {
+        // Generate `SELECT` query
+        let mut query = format!("SELECT {} FROM {}", col_names.join(", "), table);
+        if !params.is_empty() {
+            let mut where_str = Vec::with_capacity(params.len());
+            for (k, _) in params {
+                let col = &k[1..];
+                where_str.push(format!("{col} = {k}"));
+            }
+            query.push_str(&format!(" WHERE {}", where_str.join(" AND ")));
+        };
+        debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}");
+
+        // First we prepare the query
+        let conn = self.conn.lock().await;
+        let Ok(mut stmt) = conn.prepare(&query) else {
+            return Err(WalletDbError::QueryPreparationFailed)
+        };
+
+        // Execute the query using provided params
+        let Ok(mut rows) = stmt.query(params) else {
+            return Err(WalletDbError::QueryExecutionFailed)
+        };
+
+        // Check if row exists
+        let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
+        let row = match next {
+            Some(row_result) => row_result,
+            None => return Ok(vec![]),
+        };
+
+        // Grab returned values
+        let mut result = vec![];
+        for col in col_names {
+            let Ok(value) = row.get(col) else { return Err(WalletDbError::ParseColumnValueError) };
+            result.push(value);
+        }
+
+        Ok(result)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rusqlite::types::Value;
+
+    use crate::walletdb::WalletDb;
+
+    #[test]
+    fn test_mem_wallet() {
+        smol::block_on(async {
+            let wallet = WalletDb::new(None, Some("foobar")).unwrap();
+            wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).await.unwrap();
+            wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).await.unwrap();
+
+            let ret = wallet.query_single("mista", vec!["numba"], &[]).await.unwrap();
+            assert_eq!(ret.len(), 1);
+            let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 };
+            assert_eq!(numba, 42);
+        });
+    }
+
+    #[test]
+    fn test_query_single() {
+        smol::block_on(async {
+            let wallet = WalletDb::new(None, None).unwrap();
+            wallet
+                .exec_sql(
+                    "CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );",
+                    &[],
+                )
+                .await
+                .unwrap();
+
+            let why = 42;
+            let are = "are".to_string();
+            let you = 69;
+            let gae = vec![42u8; 32];
+
+            wallet
+                .exec_sql(
+                    "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
+                    rusqlite::params![why, are, you, gae],
+                )
+                .await
+                .unwrap();
+
+            let ret =
+                wallet.query_single("mista", vec!["why", "are", "you", "gae"], &[]).await.unwrap();
+            assert_eq!(ret.len(), 4);
+            assert_eq!(ret[0], Value::Integer(why));
+            assert_eq!(ret[1], Value::Text(are.clone()));
+            assert_eq!(ret[2], Value::Integer(you));
+            assert_eq!(ret[3], Value::Blob(gae.clone()));
+
+            let ret = wallet
+                .query_single(
+                    "mista",
+                    vec!["gae"],
+                    rusqlite::named_params! {":why" : why, ":are" : are, ":you" : you},
+                )
+                .await
+                .unwrap();
+            assert_eq!(ret.len(), 1);
+            assert_eq!(ret[0], Value::Blob(gae));
+        });
+    }
+}

BIN
bin/drk2/wallet.mp3