mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 22:57:59 -05:00
darkotc: Initial commit.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@
|
||||
/dao-cli
|
||||
/daod
|
||||
/darkfid
|
||||
/darkotc
|
||||
/dnetview
|
||||
/drk
|
||||
/faucetd
|
||||
|
||||
4
bin/darkotc/README.md
Normal file
4
bin/darkotc/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
darkotc
|
||||
=======
|
||||
|
||||
Commandline tool for atomic swaps.
|
||||
45
bin/darkotc/src/cli_util.rs
Normal file
45
bin/darkotc/src/cli_util.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::process::exit;
|
||||
|
||||
use num_bigint::BigUint;
|
||||
|
||||
use darkfi::{util::decode_base10, Result};
|
||||
|
||||
pub fn parse_value_pair(s: &str) -> Result<(BigUint, BigUint)> {
|
||||
let v: Vec<&str> = s.split(':').collect();
|
||||
if v.len() != 2 {
|
||||
eprintln!("Invalid value pair. Use a pair such as '13.37:11.0'.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// TODO: We shouldn't be hardcoding everything to 8 decimals.
|
||||
let val0 = decode_base10(v[0], 8, true);
|
||||
let val1 = decode_base10(v[1], 8, true);
|
||||
|
||||
if val0.is_err() || val1.is_err() {
|
||||
eprintln!("Invalid value pair. Use a pair such as '13.37:11.0'.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok((val0.unwrap(), val1.unwrap()))
|
||||
}
|
||||
|
||||
pub fn parse_token_pair(s: &str) -> Result<(String, String)> {
|
||||
let v: Vec<&str> = s.split(':').collect();
|
||||
if v.len() != 2 {
|
||||
eprintln!("Invalid token pair. Use a pair such as:");
|
||||
eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// TODO: Check if valid Fp
|
||||
let tok0 = bs58::decode(v[0]).into_vec();
|
||||
let tok1 = bs58::decode(v[1]).into_vec();
|
||||
|
||||
if tok0.is_err() || tok1.is_err() {
|
||||
eprintln!("Invalid token pair. Use a pair such as:");
|
||||
eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok((v[0].to_string(), v[1].to_string()))
|
||||
}
|
||||
320
bin/darkotc/src/main.rs
Normal file
320
bin/darkotc/src/main.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use std::{process::exit, str::FromStr};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use halo2_proofs::arithmetic::Field;
|
||||
use num_bigint::BigUint;
|
||||
use rand::rngs::OsRng;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
use darkfi::{
|
||||
cli_desc,
|
||||
crypto::{
|
||||
address::Address,
|
||||
burn_proof::create_burn_proof,
|
||||
keypair::{PublicKey, SecretKey},
|
||||
merkle_node::MerkleNode,
|
||||
mint_proof::create_mint_proof,
|
||||
proof::ProvingKey,
|
||||
token_id,
|
||||
types::{DrkCoinBlind, DrkSerial, DrkValueBlind},
|
||||
OwnCoin,
|
||||
},
|
||||
rpc::{client::RpcClient, jsonrpc::JsonRequest},
|
||||
util::{cli::progress_bar, encode_base10, serial::deserialize},
|
||||
zk::circuit::{BurnContract, MintContract},
|
||||
Result,
|
||||
};
|
||||
|
||||
mod cli_util;
|
||||
use cli_util::{parse_token_pair, parse_value_pair};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "darkotc", about = cli_desc!(), version)]
|
||||
#[clap(arg_required_else_help(true))]
|
||||
struct Args {
|
||||
#[clap(short, parse(from_occurrences))]
|
||||
/// Increase verbosity (-vvv supported)
|
||||
verbose: u8,
|
||||
|
||||
#[clap(short, long, default_value = "tcp://127.0.0.1:8340")]
|
||||
/// darkfid JSON-RPC endpoint
|
||||
endpoint: Url,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Subcmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Subcmd {
|
||||
/// Initialize an atomic swap
|
||||
Init {
|
||||
#[clap(short, long)]
|
||||
/// Pair of token IDs to swap: e.g. token_to_send:token_to_recv
|
||||
token_pair: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
/// Pair of values to swap: e.g. value_to_send:value_to_recv
|
||||
value_pair: String,
|
||||
},
|
||||
}
|
||||
|
||||
struct Rpc {
|
||||
pub rpc_client: RpcClient,
|
||||
}
|
||||
|
||||
impl Rpc {
|
||||
async fn balance_of(&self, token_id: &str) -> Result<u64> {
|
||||
let req = JsonRequest::new("wallet.get_balances", json!([]));
|
||||
let rep = self.rpc_client.request(req).await?;
|
||||
|
||||
if !rep.is_object() {
|
||||
eprintln!("Error: Invalid balance data received from darkfid RPC endpoint.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for i in rep.as_object().unwrap().keys() {
|
||||
if i == &token_id {
|
||||
if let Some(balance) = rep[i].as_u64() {
|
||||
return Ok(balance)
|
||||
}
|
||||
|
||||
eprintln!("Error: Invalid balance data received from darkfid RPC endpoint.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
async fn wallet_address(&self) -> Result<Address> {
|
||||
let req = JsonRequest::new("wallet.get_addrs", json!([0_i64]));
|
||||
let rep = self.rpc_client.request(req).await?;
|
||||
|
||||
if !rep.is_array() || !rep.as_array().unwrap()[0].is_string() {
|
||||
eprintln!("Error: Invalid wallet address received from darkfid RPC endpoint.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Address::from_str(rep[0].as_str().unwrap())
|
||||
}
|
||||
|
||||
async fn get_coins_valtok(&self, value: u64, token_id: &str) -> Result<Vec<OwnCoin>> {
|
||||
let req = JsonRequest::new("wallet.get_coins_valtok", json!([value, token_id, true]));
|
||||
let rep = self.rpc_client.request(req).await?;
|
||||
|
||||
if !rep.is_array() {
|
||||
eprintln!("Error: Invalid coin data received from darkfid RPC endpoint.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut ret = vec![];
|
||||
let rep = rep.as_array().unwrap();
|
||||
|
||||
for i in rep {
|
||||
if !i.is_string() {
|
||||
eprintln!("Error: Invalid base58 data for OwnCoin");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let data = match bs58::decode(i.as_str().unwrap()).into_vec() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed decoding base58 for OwnCoin: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let oc = match deserialize(&data) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed deserializing OwnCoin: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
ret.push(oc);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn get_merkle_path(&self, leaf_pos: usize) -> Result<Vec<MerkleNode>> {
|
||||
let req = JsonRequest::new("wallet.get_merkle_path", json!([leaf_pos as u64]));
|
||||
let rep = self.rpc_client.request(req).await?;
|
||||
|
||||
if !rep.is_array() {
|
||||
eprintln!("Error: Invalid merkle path data received from darkfid RPC endpoint.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut ret = vec![];
|
||||
let rep = rep.as_array().unwrap();
|
||||
|
||||
for i in rep {
|
||||
if !i.is_string() {
|
||||
eprintln!("Error: Invalid base58 data for MerkleNode");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let n = i.as_str().unwrap();
|
||||
let n = match bs58::decode(n).into_vec() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed decoding base58 for MerkleNode: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if n.len() != 32 {
|
||||
eprintln!("Error: MerkleNode byte length is not 32");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let n = MerkleNode::from_bytes(&n.try_into().unwrap());
|
||||
if n.is_some().unwrap_u8() == 0 {
|
||||
eprintln!("Error: Noncanonical bytes of MerkleNode");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
ret.push(n.unwrap());
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_swap(
|
||||
endpoint: Url,
|
||||
token_pair: (String, String),
|
||||
value_pair: (BigUint, BigUint),
|
||||
) -> Result<()> {
|
||||
let rpc_client = RpcClient::new(endpoint).await?;
|
||||
let rpc = Rpc { rpc_client };
|
||||
|
||||
// TODO: Rethink the use of BigUint throughout the codebase. Can we just use u64?
|
||||
// TODO: Think about decimals as well, there has to be some metadata to keep track.
|
||||
let tp = (token_id::parse_b58(&token_pair.0)?, token_id::parse_b58(&token_pair.1)?);
|
||||
let vp: (u64, u64) =
|
||||
(value_pair.0.clone().try_into().unwrap(), value_pair.1.clone().try_into().unwrap());
|
||||
|
||||
// Connect to darkfid and see if there's available funds.
|
||||
let balance = rpc.balance_of(&token_pair.0).await?;
|
||||
if balance < vp.0 {
|
||||
eprintln!(
|
||||
"Error: There is not enough balance for token \"{}\" in your wallet.",
|
||||
token_pair.0
|
||||
);
|
||||
eprintln!(
|
||||
"Available balance is {} ({})",
|
||||
encode_base10(BigUint::from(balance), 8),
|
||||
balance
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// If not enough funds in a single coin, mint a single new coin
|
||||
// with the funds. We do this to minimize the size of the swap
|
||||
// transaction, i.e. 2 inputs and 2 outputs.
|
||||
// TODO: Implement ^
|
||||
// TODO: Maybe this should be done by the user beforehand?
|
||||
|
||||
// Find a coin to spend
|
||||
let coins = rpc.get_coins_valtok(vp.0, &token_pair.0).await?;
|
||||
if coins.is_empty() {
|
||||
eprintln!("Error: Did not manage to find a coin with enough value to spend");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
eprintln!("Initializing swap data for:");
|
||||
eprintln!("Send: {} {} tokens", encode_base10(value_pair.0, 8), token_pair.0);
|
||||
eprintln!("Recv: {} {} tokens", encode_base10(value_pair.1, 8), token_pair.1);
|
||||
|
||||
// Fetch our default address
|
||||
let our_address = rpc.wallet_address().await?;
|
||||
let our_publickey = match PublicKey::try_from(our_address) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error converting our address into PublicKey: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Build proving keys
|
||||
let pb = progress_bar("Building proving key for the mint contract...");
|
||||
let mint_pk = ProvingKey::build(8, &MintContract::default());
|
||||
pb.finish();
|
||||
|
||||
let pb = progress_bar("Building proving key for the burn contract...");
|
||||
let burn_pk = ProvingKey::build(11, &BurnContract::default());
|
||||
pb.finish();
|
||||
|
||||
// The coin we want to receive.
|
||||
let recv_value_blind = DrkValueBlind::random(&mut OsRng);
|
||||
let recv_token_blind = DrkValueBlind::random(&mut OsRng);
|
||||
let recv_coin_blind = DrkCoinBlind::random(&mut OsRng);
|
||||
let recv_serial = DrkSerial::random(&mut OsRng);
|
||||
|
||||
let pb = progress_bar("Building mint proof for receiving coin");
|
||||
let (mint_proof, mint_revealed) = create_mint_proof(
|
||||
&mint_pk,
|
||||
vp.1,
|
||||
tp.1,
|
||||
recv_value_blind,
|
||||
recv_token_blind,
|
||||
recv_serial,
|
||||
recv_coin_blind,
|
||||
our_publickey,
|
||||
)?;
|
||||
pb.finish();
|
||||
|
||||
// The coin we are spending.
|
||||
// We'll spend the first one we've found.
|
||||
let coin = coins[0];
|
||||
|
||||
let pb = progress_bar("Building burn proof for spending coin");
|
||||
let signature_secret = SecretKey::random(&mut OsRng);
|
||||
let merkle_path = match rpc.get_merkle_path(usize::from(coin.leaf_position)).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get merkle path for our coin from darkfid RPC: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (burn_proof, burn_revealed) = create_burn_proof(
|
||||
&burn_pk,
|
||||
vp.0,
|
||||
tp.0,
|
||||
coin.note.value_blind,
|
||||
coin.note.token_blind,
|
||||
coin.note.serial,
|
||||
coin.note.coin_blind,
|
||||
coin.secret,
|
||||
coin.leaf_position,
|
||||
merkle_path,
|
||||
signature_secret,
|
||||
)?;
|
||||
pb.finish();
|
||||
|
||||
// Pack proofs together with pedersen commitment openings so
|
||||
// counterparty can verify correctness.
|
||||
|
||||
// Print encoded data.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
match args.command {
|
||||
Subcmd::Init { token_pair, value_pair } => {
|
||||
let token_pair = parse_token_pair(&token_pair)?;
|
||||
let value_pair = parse_value_pair(&value_pair)?;
|
||||
let init_swap_data = init_swap(args.endpoint, token_pair, value_pair).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user