Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions crates/cli-client/src/cli/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use crate::cli::{BasicCommand, Cli};
use crate::config::Config;
use crate::error::Error;

use simplicityhl::elements::pset::serialize::Serialize;
use simplicityhl::simplicity::hex::DisplayHex;

use simplicityhl_core::{LIQUID_TESTNET_GENESIS, finalize_p2pk_transaction};

impl Cli {
pub(crate) async fn run_basic(&self, config: Config, command: &BasicCommand) -> Result<(), Error> {
match command {
BasicCommand::SplitNative { parts, fee, broadcast } => {
let wallet = self.get_wallet(&config).await?;

let native_asset = simplicityhl_core::LIQUID_TESTNET_BITCOIN_ASSET;
let filter = coin_store::Filter::new()
.asset_id(native_asset)
.script_pubkey(wallet.signer().p2pk_address(config.address_params())?.script_pubkey());

let results = wallet.store().query(&[filter]).await?;

let entry = results
.into_iter()
.next()
.and_then(|r| match r {
coin_store::QueryResult::Found(entries) => entries.into_iter().next(),
coin_store::QueryResult::InsufficientValue(_) | coin_store::QueryResult::Empty => None,
})
.ok_or_else(|| Error::Config("No native UTXO found".to_string()))?;

let outpoint = entry.outpoint();
let txout = entry.txout().clone();

let pst = contracts::sdk::split_native_any((*outpoint, txout.clone()), *parts, *fee)?;

let tx = pst.extract_tx()?;
let utxos = &[txout];

let signature =
wallet
.signer()
.sign_p2pk(&tx, utxos, 0, config.address_params(), *LIQUID_TESTNET_GENESIS)?;

let tx = finalize_p2pk_transaction(
tx,
utxos,
&wallet.signer().public_key(),
&signature,
0,
config.address_params(),
*LIQUID_TESTNET_GENESIS,
)?;

if *broadcast {
cli_helper::explorer::broadcast_tx(&tx).await?;

wallet.store().mark_as_spent(*outpoint).await?;

let txid = tx.txid();
for (vout, output) in tx.output.iter().enumerate() {
if output.is_fee() {
continue;
}

#[allow(clippy::cast_possible_truncation)]
let new_outpoint = simplicityhl::elements::OutPoint::new(txid, vout as u32);

wallet.store().insert(new_outpoint, output.clone(), None).await?;
}

println!("Broadcasted: {txid}");
} else {
println!("{}", tx.serialize().to_lower_hex_string());
}

Ok(())
}
BasicCommand::TransferNative { .. } => todo!(),
BasicCommand::TransferAsset { .. } => todo!(),
BasicCommand::IssueAsset { .. } => todo!(),
BasicCommand::ReissueAsset { .. } => todo!(),
}
}
}
108 changes: 108 additions & 0 deletions crates/cli-client/src/cli/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::cli::{Cli, HelperCommand};
use crate::config::Config;
use crate::error::Error;
use crate::wallet::Wallet;

impl Cli {
pub(crate) async fn run_helper(&self, config: Config, command: &HelperCommand) -> Result<(), Error> {
match command {
HelperCommand::Init => {
let seed = self.parse_seed()?;
let db_path = config.database_path();

std::fs::create_dir_all(&config.storage.data_dir)?;
Wallet::create(&seed, &db_path, config.address_params()).await?;

println!("Wallet initialized at {}", db_path.display());
Ok(())
}
HelperCommand::Address => {
let wallet = self.get_wallet(&config).await?;

wallet.signer().print_details()?;

Ok(())
}
HelperCommand::Balance => {
let wallet = self.get_wallet(&config).await?;

let filter = coin_store::Filter::new()
.script_pubkey(wallet.signer().p2pk_address(config.address_params())?.script_pubkey());
let results = wallet.store().query(&[filter]).await?;

let mut balances: std::collections::HashMap<simplicityhl::elements::AssetId, u64> =
std::collections::HashMap::new();

if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() {
for entry in entries {
let (asset, value) = match entry {
coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value),
coin_store::UtxoEntry::Explicit { txout, .. } => {
let asset = txout.asset.explicit().unwrap();
let value = txout.value.explicit().unwrap();
(asset, value)
}
};
*balances.entry(asset).or_insert(0) += value;
}
}

if balances.is_empty() {
println!("No UTXOs found");
} else {
for (asset, value) in &balances {
println!("{asset}: {value}");
}
}
Ok(())
}
HelperCommand::Utxos => {
let wallet = self.get_wallet(&config).await?;

let filter = coin_store::Filter::new();
let results = wallet.store().query(&[filter]).await?;

if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() {
for entry in &entries {
let outpoint = entry.outpoint();
let (asset, value) = match entry {
coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value),
coin_store::UtxoEntry::Explicit { txout, .. } => {
let asset = txout.asset.explicit().unwrap();
let value = txout.value.explicit().unwrap();
(asset, value)
}
};
println!("{outpoint} | {asset} | {value}");
}
println!("Total: {} UTXOs", entries.len());
} else {
println!("No UTXOs found");
}
Ok(())
}
HelperCommand::Import { outpoint, blinding_key } => {
let wallet = self.get_wallet(&config).await?;

let txout = cli_helper::explorer::fetch_utxo(*outpoint).await?;

let blinder = match blinding_key {
Some(key_hex) => {
let bytes: [u8; 32] = hex::decode(key_hex)
.map_err(|e| Error::Config(format!("Invalid blinding key hex: {e}")))?
.try_into()
.map_err(|_| Error::Config("Blinding key must be 32 bytes".to_string()))?;
Some(bytes)
}
None => None,
};

wallet.store().insert(*outpoint, txout, blinder).await?;

println!("Imported {outpoint}");

Ok(())
}
}
}
}
124 changes: 12 additions & 112 deletions crates/cli-client/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
mod basic;
mod commands;
mod helper;

use std::path::PathBuf;

use clap::Parser;

use crate::config::{Config, default_config_path};
use crate::error::Error;
use crate::wallet::Wallet;

pub use commands::{Command, HelperCommand, MakerCommand, TakerCommand};
use crate::wallet::Wallet;
pub use commands::{BasicCommand, Command, HelperCommand, MakerCommand, TakerCommand};

#[derive(Debug, Parser)]
#[command(name = "simplicity-dex")]
Expand Down Expand Up @@ -43,11 +45,18 @@ impl Cli {
.map_err(|_| Error::Config("Seed must be exactly 32 bytes (64 hex chars)".to_string()))
}

async fn get_wallet(&self, config: &Config) -> Result<Wallet, Error> {
let seed = self.parse_seed()?;
let db_path = config.database_path();

Wallet::open(&seed, &db_path, config.address_params()).await
}

pub async fn run(&self) -> Result<(), Error> {
let config = self.load_config();

match &self.command {
Command::Basic { command: _ } => todo!(),
Command::Basic { command } => self.run_basic(config, command).await,
Command::Maker { command: _ } => todo!(),
Command::Taker { command: _ } => todo!(),
Command::Helper { command } => self.run_helper(config, command).await,
Expand All @@ -57,113 +66,4 @@ impl Cli {
}
}
}

async fn run_helper(&self, config: Config, command: &HelperCommand) -> Result<(), Error> {
match command {
HelperCommand::Init => {
let seed = self.parse_seed()?;
let db_path = config.database_path();

std::fs::create_dir_all(&config.storage.data_dir)?;
Wallet::create(&seed, &db_path, config.address_params()).await?;

println!("Wallet initialized at {}", db_path.display());
Ok(())
}
HelperCommand::Address => {
let seed = self.parse_seed()?;
let db_path = config.database_path();
let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?;

wallet.signer().print_details()?;

Ok(())
}
HelperCommand::Balance => {
let seed = self.parse_seed()?;
let db_path = config.database_path();
let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?;

let filter = coin_store::Filter::new()
.script_pubkey(wallet.signer().p2pk_address(config.address_params())?.script_pubkey());
let results = wallet.store().query(&[filter]).await?;

let mut balances: std::collections::HashMap<simplicityhl::elements::AssetId, u64> =
std::collections::HashMap::new();

if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() {
for entry in entries {
let (asset, value) = match entry {
coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value),
coin_store::UtxoEntry::Explicit { txout, .. } => {
let asset = txout.asset.explicit().unwrap();
let value = txout.value.explicit().unwrap();
(asset, value)
}
};
*balances.entry(asset).or_insert(0) += value;
}
}

if balances.is_empty() {
println!("No UTXOs found");
} else {
for (asset, value) in &balances {
println!("{asset}: {value}");
}
}
Ok(())
}
HelperCommand::Utxos => {
let seed = self.parse_seed()?;
let db_path = config.database_path();
let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?;

let filter = coin_store::Filter::new();
let results = wallet.store().query(&[filter]).await?;

if let Some(coin_store::QueryResult::Found(entries)) = results.into_iter().next() {
for entry in &entries {
let outpoint = entry.outpoint();
let (asset, value) = match entry {
coin_store::UtxoEntry::Confidential { secrets, .. } => (secrets.asset, secrets.value),
coin_store::UtxoEntry::Explicit { txout, .. } => {
let asset = txout.asset.explicit().unwrap();
let value = txout.value.explicit().unwrap();
(asset, value)
}
};
println!("{outpoint} | {asset} | {value}");
}
println!("Total: {} UTXOs", entries.len());
} else {
println!("No UTXOs found");
}
Ok(())
}
HelperCommand::Import { outpoint, blinding_key } => {
let seed = self.parse_seed()?;
let db_path = config.database_path();
let wallet = Wallet::open(&seed, &db_path, config.address_params()).await?;

let txout = cli_helper::explorer::fetch_utxo(*outpoint).await?;

let blinder = match blinding_key {
Some(key_hex) => {
let bytes: [u8; 32] = hex::decode(key_hex)
.map_err(|e| Error::Config(format!("Invalid blinding key hex: {e}")))?
.try_into()
.map_err(|_| Error::Config("Blinding key must be 32 bytes".to_string()))?;
Some(bytes)
}
None => None,
};

wallet.store().insert(*outpoint, txout, blinder).await?;

println!("Imported {outpoint}");
Ok(())
}
}
}
}
12 changes: 12 additions & 0 deletions crates/cli-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,16 @@ pub enum Error {

#[error("Explorer error: {0}")]
Explorer(#[from] cli_helper::explorer::ExplorerError),

#[error("Contract error: {0}")]
Contract(#[from] contracts::error::TransactionBuildError),

#[error("Program error: {0}")]
Program(#[from] simplicityhl_core::ProgramError),

#[error("PSET error: {0}")]
Pset(#[from] simplicityhl::elements::pset::Error),

#[error("Hex error: {0}")]
Hex(#[from] hex::FromHexError),
}
2 changes: 2 additions & 0 deletions crates/coin-store/src/entry.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use simplicityhl::elements::{OutPoint, TxOut, TxOutSecrets};

#[derive(Debug)]
pub enum UtxoEntry {
Confidential {
outpoint: OutPoint,
Expand Down Expand Up @@ -36,6 +37,7 @@ impl UtxoEntry {
}
}

#[derive(Debug)]
pub enum QueryResult {
Found(Vec<UtxoEntry>),
InsufficientValue(Vec<UtxoEntry>),
Expand Down
Loading