drk: drk rewritte skeleton added

This commit is contained in:
aggstam
2024-01-18 22:33:27 +02:00
committed by lunar-mining
parent fe28c86fa3
commit f5a9cf3e96
10 changed files with 585 additions and 1 deletions

View File

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

View File

@@ -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
bin/drk2/Cargo.toml Normal file
View File

@@ -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
bin/drk2/Makefile Normal file
View File

@@ -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
bin/drk2/drk_config.toml Normal file
View File

@@ -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
bin/drk2/src/cli_util.rs Normal file
View File

@@ -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
bin/drk2/src/error.rs Normal file
View File

@@ -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
bin/drk2/src/main.rs Normal file
View File

@@ -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
bin/drk2/src/walletdb.rs Normal file
View File

@@ -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 Normal file

Binary file not shown.