mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-06 21:34:00 -05:00
2825 lines
93 KiB
Rust
2825 lines
93 KiB
Rust
/* This file is part of DarkFi (https://dark.fi)
|
|
*
|
|
* Copyright (C) 2020-2026 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::{stdin, Read},
|
|
process::exit,
|
|
str::FromStr,
|
|
};
|
|
|
|
use prettytable::{format, row, Table};
|
|
use rand::rngs::OsRng;
|
|
use smol::{channel::unbounded, fs::read_to_string, stream::StreamExt};
|
|
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
|
|
use tracing::info;
|
|
use tracing_appender::non_blocking;
|
|
use url::Url;
|
|
|
|
use darkfi::{
|
|
async_daemonize, cli_desc,
|
|
system::ExecutorPtr,
|
|
util::{
|
|
encoding::base64,
|
|
logger::{set_terminal_writer, ChannelWriter},
|
|
parse::{decode_base10, encode_base10},
|
|
path::{expand_path, get_config_path},
|
|
},
|
|
zk::halo2::Field,
|
|
Error, Result,
|
|
};
|
|
use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction};
|
|
use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId};
|
|
use darkfi_sdk::{
|
|
crypto::{
|
|
keypair::{Address, Keypair, Network, SecretKey, StandardAddress},
|
|
note::AeadEncryptedNote,
|
|
BaseBlind, ContractId, FuncId, FuncRef, DAO_CONTRACT_ID,
|
|
},
|
|
pasta::{group::ff::PrimeField, pallas},
|
|
tx::TransactionHash,
|
|
};
|
|
use darkfi_serial::{deserialize_async, serialize_async};
|
|
|
|
use drk::{
|
|
cli_util::{
|
|
display_mining_config, generate_completions, kaching, parse_calls_from_stdin,
|
|
parse_mining_config_from_stdin, parse_token_pair, parse_tree, parse_tx_from_stdin,
|
|
parse_value_pair, print_output, tx_from_calls_mapped,
|
|
},
|
|
common::*,
|
|
dao::{DaoParams, ProposalRecord},
|
|
interactive::interactive,
|
|
money::BALANCE_BASE10_DECIMALS,
|
|
swap::PartialSwapData,
|
|
Drk,
|
|
};
|
|
|
|
const CONFIG_FILE: &str = "drk_config.toml";
|
|
const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml");
|
|
|
|
// Dev Note: when adding/modifying args here,
|
|
// don't forget to update cli_util::generate_completions()
|
|
// and interactive::help().
|
|
#[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(short, long, default_value = "testnet")]
|
|
/// Blockchain network to use
|
|
network: String,
|
|
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: Subcmd,
|
|
|
|
#[structopt(short, long)]
|
|
/// Flag indicating whether you want some fun in your life
|
|
fun: bool,
|
|
|
|
#[structopt(short, long)]
|
|
/// Set log file to ouput into
|
|
log: Option<String>,
|
|
|
|
#[structopt(short, parse(from_occurrences))]
|
|
/// Increase verbosity (-vvv supported)
|
|
verbose: u8,
|
|
}
|
|
|
|
// Dev Note: when adding/modifying commands here,
|
|
// don't forget to update cli_util::generate_completions()
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum Subcmd {
|
|
/// Enter Drk interactive shell
|
|
Interactive,
|
|
|
|
/// Fun
|
|
Kaching,
|
|
|
|
/// Send a ping request to the darkfid RPC endpoint
|
|
Ping,
|
|
|
|
/// Generate a SHELL completion script and print to stdout
|
|
Completions {
|
|
/// The Shell you want to generate script for
|
|
shell: String,
|
|
},
|
|
|
|
/// Wallet operations
|
|
Wallet {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: WalletSubcmd,
|
|
},
|
|
|
|
/// Read a transaction from stdin and mark its input coins as spent
|
|
Spend,
|
|
|
|
/// Unspend a coin
|
|
Unspend {
|
|
/// base64-encoded coin to mark as unspent
|
|
coin: String,
|
|
},
|
|
|
|
/// Create a payment transaction
|
|
Transfer {
|
|
/// Amount to send
|
|
amount: String,
|
|
|
|
/// Token ID to send
|
|
token: String,
|
|
|
|
/// Recipient address
|
|
recipient: String,
|
|
|
|
/// Optional contract spend hook to use
|
|
spend_hook: Option<String>,
|
|
|
|
/// Optional user data to use
|
|
user_data: Option<String>,
|
|
|
|
#[structopt(long)]
|
|
/// Split the output coin into two equal halves
|
|
half_split: bool,
|
|
},
|
|
|
|
/// OTC atomic swap
|
|
Otc {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: OtcSubcmd,
|
|
},
|
|
|
|
/// DAO functionalities
|
|
Dao {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: DaoSubcmd,
|
|
},
|
|
|
|
/// Attach the fee call to a transaction given from stdin
|
|
AttachFee,
|
|
|
|
/// Create a transaction from newline-separated calls from stdin
|
|
TxFromCalls {
|
|
/// Optional parent/children dependency map for the calls
|
|
calls_map: Option<String>,
|
|
},
|
|
|
|
/// Inspect a transaction from stdin
|
|
Inspect,
|
|
|
|
/// Read a transaction from stdin and broadcast it
|
|
Broadcast,
|
|
|
|
/// Scan the blockchain and parse relevant transactions
|
|
Scan {
|
|
#[structopt(long)]
|
|
/// Reset wallet state to provided block height and start scanning
|
|
reset: Option<u32>,
|
|
},
|
|
|
|
/// Explorer related subcommands
|
|
Explorer {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: ExplorerSubcmd,
|
|
},
|
|
|
|
/// Manage Token aliases
|
|
Alias {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: AliasSubcmd,
|
|
},
|
|
|
|
/// Token functionalities
|
|
Token {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: TokenSubcmd,
|
|
},
|
|
|
|
/// Contract functionalities
|
|
Contract {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: ContractSubcmd,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum WalletSubcmd {
|
|
/// Initialize wallet database
|
|
Initialize,
|
|
|
|
/// Generate a new keypair in the wallet
|
|
Keygen,
|
|
|
|
/// Query the wallet for known balances
|
|
Balance,
|
|
|
|
/// Get the default address in the wallet
|
|
Address,
|
|
|
|
/// Print all the addresses in the wallet
|
|
Addresses,
|
|
|
|
/// Set the default address in the wallet
|
|
DefaultAddress {
|
|
/// Identifier of the address
|
|
index: usize,
|
|
},
|
|
|
|
/// Print all the secret keys from the wallet
|
|
Secrets,
|
|
|
|
/// Import secret keys from stdin into the wallet, separated by newlines
|
|
ImportSecrets,
|
|
|
|
/// Print the Merkle tree in the wallet
|
|
Tree,
|
|
|
|
/// Print all the coins in the wallet
|
|
Coins,
|
|
|
|
/// Print a wallet address mining configuration
|
|
MiningConfig {
|
|
/// Identifier of the address
|
|
index: usize,
|
|
|
|
/// Optional contract spend hook to use
|
|
spend_hook: Option<String>,
|
|
|
|
/// Optional user data to use
|
|
user_data: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum OtcSubcmd {
|
|
/// Initialize the first half of the atomic swap
|
|
Init {
|
|
/// Value pair to send:recv (11.55:99.42)
|
|
#[structopt(short, long)]
|
|
value_pair: String,
|
|
|
|
/// Token pair to send:recv (f00:b4r)
|
|
#[structopt(short, long)]
|
|
token_pair: String,
|
|
},
|
|
|
|
/// Build entire swap tx given the first half from stdin
|
|
Join,
|
|
|
|
/// Inspect a swap half or the full swap tx from stdin
|
|
Inspect,
|
|
|
|
/// Sign a swap transaction given from stdin
|
|
Sign,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum DaoSubcmd {
|
|
/// Create DAO parameters
|
|
Create {
|
|
/// The minimum amount of governance tokens needed to open a proposal for this DAO
|
|
proposer_limit: String,
|
|
/// Minimal threshold of participating total tokens needed for a proposal to pass
|
|
quorum: String,
|
|
/// Minimal threshold of participating total tokens needed for a proposal to
|
|
/// be considered as strongly supported, enabling early execution.
|
|
/// Must be greater or equal to normal quorum.
|
|
early_exec_quorum: String,
|
|
/// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)
|
|
approval_ratio: f64,
|
|
/// DAO's governance token ID
|
|
gov_token_id: String,
|
|
},
|
|
|
|
/// View DAO data from stdin
|
|
View,
|
|
|
|
/// Import DAO data from stdin
|
|
Import {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
},
|
|
|
|
/// List imported DAOs (or info about a specific one)
|
|
List {
|
|
/// Name identifier for the DAO (optional)
|
|
name: Option<String>,
|
|
},
|
|
|
|
/// Show the balance of a DAO
|
|
Balance {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
},
|
|
|
|
/// Mint an imported DAO on-chain
|
|
Mint {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
},
|
|
|
|
/// Create a transfer proposal for a DAO
|
|
ProposeTransfer {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
|
|
/// Duration of the proposal, in block windows
|
|
duration: u64,
|
|
|
|
/// Amount to send
|
|
amount: String,
|
|
|
|
/// Token ID to send
|
|
token: String,
|
|
|
|
/// Recipient address
|
|
recipient: String,
|
|
|
|
/// Optional contract spend hook to use
|
|
spend_hook: Option<String>,
|
|
|
|
/// Optional user data to use
|
|
user_data: Option<String>,
|
|
},
|
|
|
|
/// Create a generic proposal for a DAO
|
|
ProposeGeneric {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
|
|
/// Duration of the proposal, in block windows
|
|
duration: u64,
|
|
|
|
/// Optional user data to use
|
|
user_data: Option<String>,
|
|
},
|
|
|
|
/// List DAO proposals
|
|
Proposals {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
},
|
|
|
|
/// View a DAO proposal data
|
|
Proposal {
|
|
/// Bulla identifier for the proposal
|
|
bulla: String,
|
|
|
|
#[structopt(long)]
|
|
/// Encrypt the proposal and encode it to base64
|
|
export: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Create the proposal transaction
|
|
mint_proposal: bool,
|
|
},
|
|
|
|
/// Import a base64 encoded and encrypted proposal from stdin
|
|
ProposalImport,
|
|
|
|
/// Vote on a given proposal
|
|
Vote {
|
|
/// Bulla identifier for the proposal
|
|
bulla: String,
|
|
|
|
/// Vote (0 for NO, 1 for YES)
|
|
vote: u8,
|
|
|
|
/// Optional vote weight (amount of governance tokens)
|
|
vote_weight: Option<String>,
|
|
},
|
|
|
|
/// Execute a DAO proposal
|
|
Exec {
|
|
/// Bulla identifier for the proposal
|
|
bulla: String,
|
|
|
|
#[structopt(long)]
|
|
/// Execute the proposal early
|
|
early: bool,
|
|
},
|
|
|
|
/// Print the DAO contract base64-encoded spend hook
|
|
SpendHook,
|
|
|
|
/// Print a DAO mining configuration
|
|
MiningConfig {
|
|
/// Name identifier for the DAO
|
|
name: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum ExplorerSubcmd {
|
|
/// Fetch a blockchain transaction by hash
|
|
FetchTx {
|
|
/// Transaction hash
|
|
tx_hash: String,
|
|
|
|
#[structopt(long)]
|
|
/// Encode transaction to base64
|
|
encode: bool,
|
|
},
|
|
|
|
/// Read a transaction from stdin and simulate it
|
|
SimulateTx,
|
|
|
|
/// Fetch broadcasted transactions history
|
|
TxsHistory {
|
|
/// Fetch specific history record (optional)
|
|
tx_hash: Option<String>,
|
|
|
|
#[structopt(long)]
|
|
/// Encode specific history record transaction to base64
|
|
encode: bool,
|
|
},
|
|
|
|
/// Remove reverted transactions from history
|
|
ClearReverted,
|
|
|
|
/// Fetch scanned blocks records
|
|
ScannedBlocks {
|
|
/// Fetch specific height record (optional)
|
|
height: Option<u32>,
|
|
},
|
|
|
|
/// Read a mining configuration from stdin and display its parts
|
|
MiningConfig,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum AliasSubcmd {
|
|
/// Create a Token alias
|
|
Add {
|
|
/// Token alias
|
|
alias: String,
|
|
|
|
/// Token to create alias for
|
|
token: String,
|
|
},
|
|
|
|
/// Print alias info of optional arguments.
|
|
/// If no argument is provided, list all the aliases in the wallet.
|
|
Show {
|
|
/// Token alias to search for
|
|
#[structopt(short, long)]
|
|
alias: Option<String>,
|
|
|
|
/// Token to search alias for
|
|
#[structopt(short, long)]
|
|
token: Option<String>,
|
|
},
|
|
|
|
/// Remove a Token alias
|
|
Remove {
|
|
/// Token alias to remove
|
|
alias: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum TokenSubcmd {
|
|
/// Import a mint authority
|
|
Import {
|
|
/// Mint authority secret key
|
|
secret_key: String,
|
|
|
|
/// Mint authority token blind
|
|
token_blind: String,
|
|
},
|
|
|
|
/// Generate a new mint authority
|
|
GenerateMint,
|
|
|
|
/// List token IDs with available mint authorities
|
|
List,
|
|
|
|
/// Mint tokens
|
|
Mint {
|
|
/// Token ID to mint
|
|
token: String,
|
|
|
|
/// Amount to mint
|
|
amount: String,
|
|
|
|
/// Recipient of the minted tokens
|
|
recipient: String,
|
|
|
|
/// Optional contract spend hook to use
|
|
spend_hook: Option<String>,
|
|
|
|
/// Optional user data to use
|
|
user_data: Option<String>,
|
|
},
|
|
|
|
/// Freeze a token mint
|
|
Freeze {
|
|
/// Token ID to freeze
|
|
token: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum ContractSubcmd {
|
|
/// Generate a new deploy authority
|
|
GenerateDeploy,
|
|
|
|
/// List deploy authorities in the wallet (or a specific one)
|
|
List {
|
|
/// Contract ID (optional)
|
|
contract_id: Option<String>,
|
|
},
|
|
|
|
/// Export a contract history record wasm bincode and deployment instruction, encoded to base64
|
|
ExportData {
|
|
/// Record transaction hash
|
|
tx_hash: String,
|
|
},
|
|
|
|
/// Deploy a smart contract
|
|
Deploy {
|
|
/// Contract ID (deploy authority)
|
|
deploy_auth: String,
|
|
|
|
/// Path to contract wasm bincode
|
|
wasm_path: String,
|
|
|
|
/// Optional path to serialized deploy instruction
|
|
deploy_ix: Option<String>,
|
|
},
|
|
|
|
/// Lock a smart contract
|
|
Lock {
|
|
/// Contract ID (deploy authority)
|
|
deploy_auth: String,
|
|
},
|
|
}
|
|
|
|
/// Defines a blockchain network configuration.
|
|
/// Default values correspond to a local network.
|
|
#[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)]
|
|
#[structopt()]
|
|
struct BlockchainNetwork {
|
|
#[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/cache")]
|
|
/// Path to blockchain cache database
|
|
cache_path: String,
|
|
|
|
#[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/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:28345")]
|
|
/// darkfid JSON-RPC endpoint
|
|
endpoint: Url,
|
|
|
|
#[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")]
|
|
/// Path to interactive shell history file
|
|
history_path: String,
|
|
}
|
|
|
|
/// Auxiliary function to parse darkfid configuration file and extract requested
|
|
/// blockchain network config.
|
|
async fn parse_blockchain_config(
|
|
config: Option<String>,
|
|
network: &str,
|
|
) -> Result<(Network, BlockchainNetwork)> {
|
|
// Grab network
|
|
let used_net = match network {
|
|
"mainnet" | "localnet" => Network::Mainnet,
|
|
"testnet" => Network::Testnet,
|
|
_ => return Err(Error::ParseFailed("Invalid blockchain network")),
|
|
};
|
|
|
|
// Grab config path
|
|
let config_path = get_config_path(config, CONFIG_FILE)?;
|
|
|
|
// Parse TOML file contents
|
|
let contents = read_to_string(&config_path).await?;
|
|
let contents: toml::Value = match toml::from_str(&contents) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Failed parsing TOML config: {e}");
|
|
return Err(Error::ParseFailed("Failed parsing TOML config"))
|
|
}
|
|
};
|
|
|
|
// Grab requested network config
|
|
let Some(table) = contents.as_table() else { return Err(Error::ParseFailed("TOML not a map")) };
|
|
let Some(network_configs) = table.get("network_config") else {
|
|
return Err(Error::ParseFailed("TOML does not contain network configurations"))
|
|
};
|
|
let Some(network_configs) = network_configs.as_table() else {
|
|
return Err(Error::ParseFailed("`network_config` not a map"))
|
|
};
|
|
let Some(network_config) = network_configs.get(network) else {
|
|
return Err(Error::ParseFailed("TOML does not contain requested network configuration"))
|
|
};
|
|
let network_config = toml::to_string(&network_config).unwrap();
|
|
let network_config =
|
|
match BlockchainNetwork::from_iter_with_toml::<Vec<String>>(&network_config, vec![]) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Failed parsing requested network configuration: {e}");
|
|
return Err(Error::ParseFailed("Failed parsing requested network configuration"))
|
|
}
|
|
};
|
|
|
|
Ok((used_net, network_config))
|
|
}
|
|
|
|
/// Auxiliary function to create a `Drk` wallet for provided configuration.
|
|
async fn new_wallet(
|
|
network: Network,
|
|
cache_path: String,
|
|
wallet_path: String,
|
|
wallet_pass: String,
|
|
endpoint: Option<Url>,
|
|
ex: &ExecutorPtr,
|
|
fun: bool,
|
|
) -> Drk {
|
|
// Script kiddies protection
|
|
if wallet_pass == "changeme" {
|
|
eprintln!("Please don't use default wallet password...");
|
|
exit(2);
|
|
}
|
|
|
|
match Drk::new(network, cache_path, wallet_path, wallet_pass, endpoint, ex, fun).await {
|
|
Ok(wallet) => wallet,
|
|
Err(e) => {
|
|
eprintln!("Error initializing wallet: {e}");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
async_daemonize!(realmain);
|
|
async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> {
|
|
// Grab blockchain network configuration
|
|
let (network, blockchain_config) = match args.network.as_str() {
|
|
"localnet" => parse_blockchain_config(args.config, "localnet").await?,
|
|
"testnet" => parse_blockchain_config(args.config, "testnet").await?,
|
|
"mainnet" => parse_blockchain_config(args.config, "mainnet").await?,
|
|
_ => {
|
|
eprintln!("Unsupported chain `{}`", args.network);
|
|
return Err(Error::UnsupportedChain)
|
|
}
|
|
};
|
|
|
|
match args.command {
|
|
Subcmd::Interactive => {
|
|
// Create an unbounded smol channel, so we can have a
|
|
// printing queue the background logger and tasks can
|
|
// submit messages to so the shell prints them.
|
|
let (shell_sender, shell_receiver) = unbounded();
|
|
|
|
// Set the logging writer
|
|
let (non_blocking, _guard) =
|
|
non_blocking(ChannelWriter { sender: shell_sender.clone() });
|
|
set_terminal_writer(args.verbose, non_blocking)?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint.clone()),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await
|
|
.into_ptr();
|
|
|
|
interactive(
|
|
&drk,
|
|
&blockchain_config.endpoint,
|
|
&blockchain_config.history_path,
|
|
&shell_sender,
|
|
&shell_receiver,
|
|
&ex,
|
|
)
|
|
.await;
|
|
|
|
drk.read().await.stop_rpc_client().await?;
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Kaching => {
|
|
if !args.fun {
|
|
println!("Apparently you don't like fun...");
|
|
return Ok(())
|
|
}
|
|
kaching().await;
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Ping => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.ping(&mut output).await {
|
|
print_output(&output);
|
|
return Err(e)
|
|
};
|
|
print_output(&output);
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Completions { shell } => {
|
|
println!("{}", generate_completions(&shell)?);
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Wallet { command } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
match command {
|
|
WalletSubcmd::Initialize => {
|
|
if let Err(e) = drk.initialize_wallet().await {
|
|
eprintln!("Error initializing wallet: {e}");
|
|
exit(2);
|
|
}
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.initialize_money(&mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to initialize Money: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
if let Err(e) = drk.initialize_dao().await {
|
|
eprintln!("Failed to initialize DAO: {e}");
|
|
exit(2);
|
|
}
|
|
if let Err(e) = drk.initialize_deployooor() {
|
|
eprintln!("Failed to initialize Deployooor: {e}");
|
|
exit(2);
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::Keygen => {
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.money_keygen(&mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to generate keypair: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
}
|
|
|
|
WalletSubcmd::Balance => {
|
|
let balmap = drk.money_balance().await?;
|
|
|
|
let aliases_map = drk.get_aliases_mapped_by_token().await?;
|
|
|
|
// Create a prettytable with the new data:
|
|
let mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row!["Token ID", "Aliases", "Balance"]);
|
|
for (token_id, balance) in balmap.iter() {
|
|
let aliases = match aliases_map.get(token_id) {
|
|
Some(a) => a,
|
|
None => "-",
|
|
};
|
|
|
|
table.add_row(row![
|
|
token_id,
|
|
aliases,
|
|
encode_base10(*balance, BALANCE_BASE10_DECIMALS)
|
|
]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No unspent balances found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::Address => match drk.default_address().await {
|
|
Ok(public_key) => {
|
|
let address: Address =
|
|
StandardAddress::from_public(drk.network, public_key).into();
|
|
println!("{address}");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch default address: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
|
|
WalletSubcmd::Addresses => {
|
|
let addresses = drk.addresses().await?;
|
|
let table = prettytable_addrs(drk.network, &addresses);
|
|
|
|
if table.is_empty() {
|
|
println!("No addresses found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::DefaultAddress { index } => {
|
|
if let Err(e) = drk.set_default_address(index) {
|
|
eprintln!("Failed to set default address: {e}");
|
|
exit(2);
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::Secrets => {
|
|
for secret in drk.get_money_secrets().await? {
|
|
println!("{secret}");
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::ImportSecrets => {
|
|
let mut secrets = vec![];
|
|
let lines = stdin().lines();
|
|
for (i, line) in lines.enumerate() {
|
|
if let Ok(line) = line {
|
|
let bytes = bs58::decode(&line.trim()).into_vec()?;
|
|
let Ok(secret) = deserialize_async(&bytes).await else {
|
|
println!("Warning: Failed to deserialize secret on line {i}");
|
|
continue
|
|
};
|
|
secrets.push(secret);
|
|
}
|
|
}
|
|
|
|
let mut output = vec![];
|
|
let pubkeys = match drk.import_money_secrets(secrets, &mut output).await {
|
|
Ok(p) => {
|
|
print_output(&output);
|
|
p
|
|
}
|
|
Err(e) => {
|
|
print_output(&output);
|
|
eprintln!("Failed to import secret keys into wallet: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
for key in pubkeys {
|
|
println!("{key}");
|
|
}
|
|
}
|
|
|
|
WalletSubcmd::Tree => {
|
|
println!("{:#?}", drk.get_money_tree().await?);
|
|
}
|
|
|
|
WalletSubcmd::Coins => {
|
|
let coins = drk.get_coins(true).await?;
|
|
if coins.is_empty() {
|
|
return Ok(())
|
|
}
|
|
let aliases_map = drk.get_aliases_mapped_by_token().await?;
|
|
let table = prettytable_coins(&coins, &aliases_map);
|
|
println!("{table}");
|
|
}
|
|
|
|
WalletSubcmd::MiningConfig { index, spend_hook, user_data } => {
|
|
let spend_hook = match spend_hook {
|
|
Some(s) => match FuncId::from_str(&s) {
|
|
Ok(s) => Some(s),
|
|
Err(e) => {
|
|
eprintln!("Invalid spend hook: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let user_data = match user_data {
|
|
Some(u) => {
|
|
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid user data: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => Some(v),
|
|
None => {
|
|
eprintln!("Invalid user data");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let mut output = vec![];
|
|
if let Err(e) =
|
|
drk.mining_config(index, spend_hook, user_data, &mut output).await
|
|
{
|
|
print_output(&output);
|
|
eprintln!("Failed to generate wallet mining configuration: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Spend => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to mark transaction coins as spent: {e}");
|
|
exit(2);
|
|
};
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Unspend { coin } => {
|
|
let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid coin: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => v,
|
|
None => {
|
|
eprintln!("Invalid coin");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let coin = Coin::from(elem);
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.unspend_coin(&coin).await {
|
|
eprintln!("Failed to mark coin as unspent: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Transfer { amount, token, recipient, spend_hook, user_data, half_split } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = f64::from_str(&amount) {
|
|
eprintln!("Invalid amount: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
let rcpt = match Address::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
if rcpt.network() != drk.network {
|
|
eprintln!("Recipient address prefix mismatch");
|
|
exit(2);
|
|
}
|
|
|
|
let token_id = match drk.get_token(token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Invalid token alias: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let spend_hook = match spend_hook {
|
|
Some(s) => match FuncId::from_str(&s) {
|
|
Ok(s) => Some(s),
|
|
Err(e) => {
|
|
eprintln!("Invalid spend hook: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let user_data = match user_data {
|
|
Some(u) => {
|
|
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid user data: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => Some(v),
|
|
None => {
|
|
eprintln!("Invalid user data");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let tx = match drk
|
|
.transfer(&amount, token_id, *rcpt.public_key(), spend_hook, user_data, half_split)
|
|
.await
|
|
{
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Failed to create payment transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Otc { command } => match command {
|
|
OtcSubcmd::Init { value_pair, token_pair } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let value_pair = parse_value_pair(&value_pair)?;
|
|
let token_pair = parse_token_pair(&drk, &token_pair).await?;
|
|
|
|
let half = match drk.init_swap(value_pair, token_pair, None, None, None).await {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
eprintln!("Failed to create swap transaction half: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&half).await));
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
OtcSubcmd::Join => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let Some(bytes) = base64::decode(buf.trim()) else {
|
|
eprintln!("Failed to decode partial swap data");
|
|
exit(2);
|
|
};
|
|
|
|
let partial: PartialSwapData = deserialize_async(&bytes).await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let tx = match drk.join_swap(partial, None, None, None).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create a join swap transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
OtcSubcmd::Inspect => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let Some(bytes) = base64::decode(buf.trim()) else {
|
|
eprintln!("Failed to decode swap transaction");
|
|
exit(2);
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.inspect_swap(bytes, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to inspect swap: {e}");
|
|
exit(2);
|
|
};
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
OtcSubcmd::Sign => {
|
|
let mut tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.sign_swap(&mut tx).await {
|
|
eprintln!("Failed to sign joined swap transaction: {e}");
|
|
exit(2);
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
Ok(())
|
|
}
|
|
},
|
|
|
|
Subcmd::Dao { command } => match command {
|
|
DaoSubcmd::Create {
|
|
proposer_limit,
|
|
quorum,
|
|
early_exec_quorum,
|
|
approval_ratio,
|
|
gov_token_id,
|
|
} => {
|
|
if let Err(e) = f64::from_str(&proposer_limit) {
|
|
eprintln!("Invalid proposer limit: {e}");
|
|
exit(2);
|
|
}
|
|
if let Err(e) = f64::from_str(&quorum) {
|
|
eprintln!("Invalid quorum: {e}");
|
|
exit(2);
|
|
}
|
|
if let Err(e) = f64::from_str(&early_exec_quorum) {
|
|
eprintln!("Invalid early exec quorum: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?;
|
|
let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?;
|
|
let early_exec_quorum =
|
|
decode_base10(&early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?;
|
|
|
|
if approval_ratio > 1.0 {
|
|
eprintln!("Error: Approval ratio cannot be >1.0");
|
|
exit(2);
|
|
}
|
|
|
|
let approval_ratio_base = 100_u64;
|
|
let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let gov_token_id = match drk.get_token(gov_token_id).await {
|
|
Ok(g) => g,
|
|
Err(e) => {
|
|
eprintln!("Invalid Token ID: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let notes_keypair = Keypair::random(&mut OsRng);
|
|
let proposer_keypair = Keypair::random(&mut OsRng);
|
|
let proposals_keypair = Keypair::random(&mut OsRng);
|
|
let votes_keypair = Keypair::random(&mut OsRng);
|
|
let exec_keypair = Keypair::random(&mut OsRng);
|
|
let early_exec_keypair = Keypair::random(&mut OsRng);
|
|
let bulla_blind = BaseBlind::random(&mut OsRng);
|
|
|
|
let params = DaoParams::new(
|
|
proposer_limit,
|
|
quorum,
|
|
early_exec_quorum,
|
|
approval_ratio_base,
|
|
approval_ratio_quot,
|
|
gov_token_id,
|
|
Some(notes_keypair.secret),
|
|
notes_keypair.public,
|
|
Some(proposer_keypair.secret),
|
|
proposer_keypair.public,
|
|
Some(proposals_keypair.secret),
|
|
proposals_keypair.public,
|
|
Some(votes_keypair.secret),
|
|
votes_keypair.public,
|
|
Some(exec_keypair.secret),
|
|
exec_keypair.public,
|
|
Some(early_exec_keypair.secret),
|
|
early_exec_keypair.public,
|
|
bulla_blind,
|
|
);
|
|
|
|
println!("{}", params.toml_str());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::View => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let params = DaoParams::from_toml_str(&buf)?;
|
|
println!("{params}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Import { name } => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let params = DaoParams::from_toml_str(&buf)?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.import_dao(&name, ¶ms, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to import DAO: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::List { name } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.dao_list(&name, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to list DAO: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Balance { name } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let balmap = match drk.dao_balance(&name).await {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch DAO balance: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let aliases_map = match drk.get_aliases_mapped_by_token().await {
|
|
Ok(a) => a,
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch wallet aliases: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let table = prettytable_balance(&balmap, &aliases_map);
|
|
|
|
if table.is_empty() {
|
|
println!("No unspent balances found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Mint { name } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let tx = match drk.dao_mint(&name).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to mint DAO: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::ProposeTransfer {
|
|
name,
|
|
duration,
|
|
amount,
|
|
token,
|
|
recipient,
|
|
spend_hook,
|
|
user_data,
|
|
} => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = f64::from_str(&amount) {
|
|
eprintln!("Invalid amount: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
let rcpt = match Address::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
if rcpt.network() != drk.network {
|
|
eprintln!("Recipient address prefix mismatch");
|
|
exit(2);
|
|
}
|
|
|
|
let token_id = match drk.get_token(token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Invalid token alias: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let spend_hook = match spend_hook {
|
|
Some(s) => match FuncId::from_str(&s) {
|
|
Ok(s) => Some(s),
|
|
Err(e) => {
|
|
eprintln!("Invalid spend hook: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let user_data = match user_data {
|
|
Some(u) => {
|
|
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid user data: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => Some(v),
|
|
None => {
|
|
eprintln!("Invalid user data");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let proposal = match drk
|
|
.dao_propose_transfer(
|
|
&name,
|
|
duration,
|
|
&amount,
|
|
token_id,
|
|
*rcpt.public_key(),
|
|
spend_hook,
|
|
user_data,
|
|
)
|
|
.await
|
|
{
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Failed to create DAO transfer proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Generated proposal: {}", proposal.bulla());
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::ProposeGeneric { name, duration, user_data } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let user_data = match user_data {
|
|
Some(u) => {
|
|
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid user data: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => Some(v),
|
|
None => {
|
|
eprintln!("Invalid user data");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let proposal = match drk.dao_propose_generic(&name, duration, user_data).await {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Failed to create DAO generic proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Generated proposal: {}", proposal.bulla());
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::Proposals { name } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let proposals = drk.get_dao_proposals(&name).await?;
|
|
|
|
for (i, proposal) in proposals.iter().enumerate() {
|
|
println!("{i}. {}", proposal.bulla());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Proposal { bulla, export, mint_proposal } => {
|
|
let bulla = match DaoProposalBulla::from_str(&bulla) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid proposal bulla: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?;
|
|
|
|
if export {
|
|
// Retrieve the DAO
|
|
let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?;
|
|
|
|
// Encypt the proposal
|
|
let enc_note = AeadEncryptedNote::encrypt(
|
|
&proposal,
|
|
&dao.params.dao.proposals_public_key,
|
|
&mut OsRng,
|
|
)
|
|
.unwrap();
|
|
|
|
// Export it to base64
|
|
println!("{}", base64::encode(&serialize_async(&enc_note).await));
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
|
|
if mint_proposal {
|
|
// Identify proposal type by its auth calls
|
|
for call in &proposal.proposal.auth_calls {
|
|
// We only support transfer right now
|
|
if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
|
|
let tx = match drk.dao_transfer_proposal_tx(&proposal).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create DAO transfer proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
}
|
|
|
|
// If proposal has no auth calls, we consider it a generic one
|
|
if proposal.proposal.auth_calls.is_empty() {
|
|
let tx = match drk.dao_generic_proposal_tx(&proposal).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create DAO generic proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
|
|
eprintln!("Unsuported DAO proposal");
|
|
exit(2);
|
|
}
|
|
|
|
println!("{proposal}");
|
|
|
|
let mut contract_calls = "\nInvoked contracts:\n".to_string();
|
|
for call in proposal.proposal.auth_calls {
|
|
contract_calls.push_str(&format!(
|
|
"\tContract: {}\n\tFunction: {}\n\tData: ",
|
|
call.contract_id, call.function_code
|
|
));
|
|
|
|
if call.auth_data.is_empty() {
|
|
contract_calls.push_str("-\n");
|
|
continue;
|
|
}
|
|
|
|
if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
|
|
// We know that the plaintext data live in the data plaintext vec
|
|
if proposal.data.is_none() {
|
|
contract_calls.push_str("-\n");
|
|
continue;
|
|
}
|
|
let coin: CoinAttributes =
|
|
deserialize_async(proposal.data.as_ref().unwrap()).await?;
|
|
let recipient: Address =
|
|
StandardAddress::from_public(drk.network, coin.public_key).into();
|
|
let spend_hook = if coin.spend_hook == FuncId::none() {
|
|
"-".to_string()
|
|
} else {
|
|
format!("{}", coin.spend_hook)
|
|
};
|
|
|
|
let user_data = if coin.user_data == pallas::Base::ZERO {
|
|
"-".to_string()
|
|
} else {
|
|
format!("{:?}", coin.user_data)
|
|
};
|
|
|
|
contract_calls.push_str(&format!("\n\t\t{}: {}\n\t\t{}: {} ({})\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\n",
|
|
"Recipient",
|
|
recipient,
|
|
"Amount",
|
|
coin.value,
|
|
encode_base10(coin.value, BALANCE_BASE10_DECIMALS),
|
|
"Token",
|
|
coin.token_id,
|
|
"Spend hook",
|
|
spend_hook,
|
|
"User data",
|
|
user_data,
|
|
"Blind",
|
|
coin.blind));
|
|
}
|
|
}
|
|
|
|
println!("{contract_calls}");
|
|
|
|
let votes = drk.get_dao_proposal_votes(&bulla).await?;
|
|
let mut total_yes_vote_value = 0;
|
|
let mut total_no_vote_value = 0;
|
|
let mut total_all_vote_value = 0;
|
|
let mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row!["Transaction", "Tokens", "Vote"]);
|
|
for vote in votes {
|
|
let vote_option = if vote.vote_option {
|
|
total_yes_vote_value += vote.all_vote_value;
|
|
"Yes"
|
|
} else {
|
|
total_no_vote_value += vote.all_vote_value;
|
|
"No"
|
|
};
|
|
total_all_vote_value += vote.all_vote_value;
|
|
|
|
table.add_row(row![
|
|
vote.tx_hash,
|
|
encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS),
|
|
vote_option
|
|
]);
|
|
}
|
|
|
|
let outcome = if table.is_empty() {
|
|
println!("Votes: No votes found");
|
|
"Unknown"
|
|
} else {
|
|
println!("Votes:");
|
|
println!("{table}");
|
|
println!(
|
|
"Total tokens votes: {}",
|
|
encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS)
|
|
);
|
|
let approval_ratio =
|
|
(total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64;
|
|
println!(
|
|
"Total tokens Yes votes: {} ({approval_ratio:.2}%)",
|
|
encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS)
|
|
);
|
|
println!(
|
|
"Total tokens No votes: {} ({:.2}%)",
|
|
encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS),
|
|
(total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64
|
|
);
|
|
|
|
let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?;
|
|
if total_all_vote_value >= dao.params.dao.quorum &&
|
|
approval_ratio >=
|
|
(dao.params.dao.approval_ratio_quot /
|
|
dao.params.dao.approval_ratio_base)
|
|
as f64
|
|
{
|
|
"Approved"
|
|
} else {
|
|
"Rejected"
|
|
}
|
|
};
|
|
|
|
if let Some(exec_tx_hash) = proposal.exec_tx_hash {
|
|
println!("Proposal was executed on transaction: {exec_tx_hash}");
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
|
|
// Retrieve next block height and current block time target,
|
|
// to compute their window.
|
|
let next_block_height = drk.get_next_block_height().await?;
|
|
let block_target = drk.get_block_target().await?;
|
|
let current_window = blockwindow(next_block_height, block_target);
|
|
let end_time = proposal.proposal.creation_blockwindow +
|
|
proposal.proposal.duration_blockwindows;
|
|
let (voting_status, proposal_status_message) = if current_window < end_time {
|
|
("Ongoing", format!("Current proposal outcome: {outcome}"))
|
|
} else {
|
|
("Concluded", format!("Proposal outcome: {outcome}"))
|
|
};
|
|
println!("Voting status: {voting_status}");
|
|
println!("{proposal_status_message}");
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::ProposalImport => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let Some(bytes) = base64::decode(buf.trim()) else {
|
|
eprintln!("Failed to decode encrypted proposal data");
|
|
exit(2);
|
|
};
|
|
let encrypted_proposal: AeadEncryptedNote = deserialize_async(&bytes).await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
// Retrieve all DAOs to try to decrypt the proposal
|
|
let daos = drk.get_daos().await?;
|
|
for dao in &daos {
|
|
// Check if we have the proposals key
|
|
let Some(proposals_secret_key) = dao.params.proposals_secret_key else {
|
|
continue
|
|
};
|
|
|
|
// Try to decrypt the proposal
|
|
let Ok(proposal) =
|
|
encrypted_proposal.decrypt::<ProposalRecord>(&proposals_secret_key)
|
|
else {
|
|
continue
|
|
};
|
|
|
|
let proposal = match drk.get_dao_proposal_by_bulla(&proposal.bulla()).await {
|
|
Ok(p) => {
|
|
let mut our_proposal = p;
|
|
our_proposal.data = proposal.data;
|
|
our_proposal
|
|
}
|
|
Err(_) => proposal,
|
|
};
|
|
|
|
return drk.put_dao_proposal(&proposal).await
|
|
}
|
|
|
|
eprintln!("Couldn't decrypt the proposal with out DAO keys");
|
|
exit(2);
|
|
}
|
|
|
|
DaoSubcmd::Vote { bulla, vote, vote_weight } => {
|
|
let bulla = match DaoProposalBulla::from_str(&bulla) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid proposal bulla: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
if vote > 1 {
|
|
eprintln!("Vote can be either 0 (NO) or 1 (YES)");
|
|
exit(2);
|
|
}
|
|
let vote = vote != 0;
|
|
|
|
let weight = match vote_weight {
|
|
Some(w) => {
|
|
if let Err(e) = f64::from_str(&w) {
|
|
eprintln!("Invalid vote weight: {e}");
|
|
exit(2);
|
|
}
|
|
Some(decode_base10(&w, BALANCE_BASE10_DECIMALS, true)?)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let tx = match drk.dao_vote(&bulla, vote, weight).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create DAO Vote transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::Exec { bulla, early } => {
|
|
let bulla = match DaoProposalBulla::from_str(&bulla) {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid proposal bulla: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?;
|
|
|
|
// Identify proposal type by its auth calls
|
|
for call in &proposal.proposal.auth_calls {
|
|
// We only support transfer right now
|
|
if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
|
|
let tx = match drk.dao_exec_transfer(&proposal, early).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to execute DAO transfer proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
}
|
|
|
|
// If proposal has no auth calls, we consider it a generic one
|
|
if proposal.proposal.auth_calls.is_empty() {
|
|
let tx = match drk.dao_exec_generic(&proposal, early).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to execute DAO generic proposal: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
return drk.stop_rpc_client().await
|
|
}
|
|
|
|
eprintln!("Unsuported DAO proposal");
|
|
exit(2);
|
|
}
|
|
|
|
DaoSubcmd::SpendHook => {
|
|
let spend_hook =
|
|
FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
|
|
.to_func_id();
|
|
|
|
println!("{spend_hook}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::MiningConfig { name } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.dao_mining_config(&name, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to generate DAO mining configuration: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
},
|
|
|
|
Subcmd::AttachFee => {
|
|
let mut tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.attach_fee(&mut tx).await {
|
|
eprintln!("Failed to attach the fee call to the transaction: {e}");
|
|
exit(2);
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::TxFromCalls { calls_map } => {
|
|
// Parse calls
|
|
let calls = parse_calls_from_stdin().await?;
|
|
if calls.is_empty() {
|
|
eprintln!("No calls were parsed");
|
|
exit(1);
|
|
}
|
|
|
|
// If there is a given map, parse it, otherwise construct a
|
|
// linear map.
|
|
let calls_map = match calls_map {
|
|
Some(cmap) => match parse_tree(&cmap) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Failed parsing calls map: {e}");
|
|
exit(1);
|
|
}
|
|
},
|
|
None => {
|
|
let mut calls_map = Vec::with_capacity(calls.len());
|
|
for (i, _) in calls.iter().enumerate() {
|
|
calls_map.push((i, vec![]));
|
|
}
|
|
calls_map
|
|
}
|
|
};
|
|
|
|
if calls_map.len() != calls.len() {
|
|
eprintln!("Calls map size not equal to parsed calls");
|
|
exit(1);
|
|
}
|
|
|
|
// Create a transaction from the mapped calls.
|
|
let (mut tx_builder, signature_secrets) = tx_from_calls_mapped(&calls, &calls_map)?;
|
|
|
|
// Now build and sign the tx
|
|
let mut tx = tx_builder.build()?;
|
|
let sigs = tx.create_sigs(&signature_secrets)?;
|
|
tx.signatures.push(sigs);
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Inspect => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
println!("{tx:#?}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Broadcast => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = drk.simulate_tx(&tx).await {
|
|
eprintln!("Failed to simulate tx: {e}");
|
|
exit(2);
|
|
};
|
|
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to mark transaction coins as spent: {e}");
|
|
exit(2);
|
|
};
|
|
|
|
let txid = match drk.broadcast_tx(&tx, &mut output).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
print_output(&output);
|
|
eprintln!("Failed to broadcast transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
print_output(&output);
|
|
|
|
println!("Transaction ID: {txid}");
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Scan { reset } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Some(height) = reset {
|
|
let mut buf = vec![];
|
|
if let Err(e) = drk.reset_to_height(height, &mut buf).await {
|
|
print_output(&buf);
|
|
eprintln!("Failed during wallet reset: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&buf);
|
|
}
|
|
|
|
if let Err(e) = drk.scan_blocks(&mut vec![], None, &true).await {
|
|
eprintln!("Failed during scanning: {e}");
|
|
exit(2);
|
|
}
|
|
println!("Finished scanning blockchain");
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Explorer { command } => match command {
|
|
ExplorerSubcmd::FetchTx { tx_hash, encode } => {
|
|
let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes());
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let tx = match drk.get_tx(&tx_hash).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let Some(tx) = tx else {
|
|
println!("Transaction was not found");
|
|
exit(1);
|
|
};
|
|
|
|
// Make sure the tx is correct
|
|
assert_eq!(tx.hash(), tx_hash);
|
|
|
|
if encode {
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
exit(1)
|
|
}
|
|
|
|
println!("Transaction ID: {tx_hash}");
|
|
println!("{tx:?}");
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
ExplorerSubcmd::SimulateTx => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let is_valid = match drk.simulate_tx(&tx).await {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Failed to simulate tx: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Transaction ID: {}", tx.hash());
|
|
println!("State: {}", if is_valid { "valid" } else { "invalid" });
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
ExplorerSubcmd::TxsHistory { tx_hash, encode } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Some(c) = tx_hash {
|
|
let (tx_hash, status, block_height, tx) = drk.get_tx_history_record(&c).await?;
|
|
|
|
if encode {
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
exit(1)
|
|
}
|
|
|
|
println!("Transaction ID: {tx_hash}");
|
|
println!("Status: {status}");
|
|
match block_height {
|
|
Some(block_height) => println!("Block height: {block_height}"),
|
|
None => println!("Block height: -"),
|
|
}
|
|
println!("{tx:?}");
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
let map = match drk.get_txs_history() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
eprintln!("Failed to retrieve transactions history records: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
// Create a prettytable with the new data:
|
|
let mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row!["Transaction Hash", "Status", "Block Height"]);
|
|
for (txs_hash, status, block_height) in map.iter() {
|
|
let block_height = match block_height {
|
|
Some(block_height) => block_height.to_string(),
|
|
None => String::from("-"),
|
|
};
|
|
table.add_row(row![txs_hash, status, block_height]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No transactions found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ExplorerSubcmd::ClearReverted => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.remove_reverted_txs(&mut output) {
|
|
print_output(&output);
|
|
eprintln!("Failed to remove reverted transactions: {e}");
|
|
exit(2);
|
|
};
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ExplorerSubcmd::ScannedBlocks { height } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Some(height) = height {
|
|
let (hash, signing_key) = match drk.get_scanned_block(&height) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Failed to retrieve scanned block record: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Height: {height}");
|
|
println!("Hash: {hash}");
|
|
println!("Signing key: {signing_key}");
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
let map = match drk.get_scanned_block_records() {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
eprintln!("Failed to retrieve scanned blocks records: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let table = prettytable_scanned_blocks(&map);
|
|
|
|
if table.is_empty() {
|
|
println!("No scanned blocks records found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ExplorerSubcmd::MiningConfig => {
|
|
let (config, recipient, spend_hook, user_data) =
|
|
parse_mining_config_from_stdin().await?;
|
|
let mut output = vec![];
|
|
display_mining_config(&config, &recipient, &spend_hook, &user_data, &mut output);
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
},
|
|
|
|
Subcmd::Alias { command } => match command {
|
|
AliasSubcmd::Add { alias, token } => {
|
|
if alias.chars().count() > 5 {
|
|
eprintln!("Error: Alias exceeds 5 characters");
|
|
exit(2);
|
|
}
|
|
|
|
let token_id = match TokenId::from_str(token.as_str()) {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Invalid Token ID: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.add_alias(alias, token_id, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to add alias: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
AliasSubcmd::Show { alias, token } => {
|
|
let token_id = match token {
|
|
Some(t) => match TokenId::from_str(t.as_str()) {
|
|
Ok(t) => Some(t),
|
|
Err(e) => {
|
|
eprintln!("Invalid Token ID: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let map = drk.get_aliases(alias, token_id).await?;
|
|
|
|
let table = prettytable_aliases(&map);
|
|
|
|
if table.is_empty() {
|
|
println!("No aliases found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
AliasSubcmd::Remove { alias } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.remove_alias(alias, &mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Failed to remove alias: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
},
|
|
|
|
Subcmd::Token { command } => match command {
|
|
TokenSubcmd::Import { secret_key, token_blind } => {
|
|
let mint_authority = match SecretKey::from_str(&secret_key) {
|
|
Ok(ma) => ma,
|
|
Err(e) => {
|
|
eprintln!("Invalid mint authority: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let token_blind = match BaseBlind::from_str(&token_blind) {
|
|
Ok(tb) => tb,
|
|
Err(e) => {
|
|
eprintln!("Invalid token blind: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let token_id = drk.import_mint_authority(mint_authority, token_blind).await?;
|
|
println!("Successfully imported mint authority for token ID: {token_id}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
TokenSubcmd::GenerateMint => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let mint_authority = SecretKey::random(&mut OsRng);
|
|
let token_blind = BaseBlind::random(&mut OsRng);
|
|
let token_id = drk.import_mint_authority(mint_authority, token_blind).await?;
|
|
println!("Successfully imported mint authority for token ID: {token_id}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
TokenSubcmd::List => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let tokens = drk.get_mint_authorities().await?;
|
|
let aliases_map = match drk.get_aliases_mapped_by_token().await {
|
|
Ok(map) => map,
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch wallet aliases: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let table = prettytable_tokenlist(&tokens, &aliases_map);
|
|
|
|
if table.is_empty() {
|
|
println!("No tokens found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
TokenSubcmd::Mint { token, amount, recipient, spend_hook, user_data } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = f64::from_str(&amount) {
|
|
eprintln!("Invalid amount: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
let rcpt = match Address::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
if rcpt.network() != drk.network {
|
|
eprintln!("Recipient address prefix mismatch");
|
|
exit(2);
|
|
}
|
|
|
|
let token_id = match drk.get_token(token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Invalid Token ID: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let spend_hook = match spend_hook {
|
|
Some(s) => match FuncId::from_str(&s) {
|
|
Ok(s) => Some(s),
|
|
Err(e) => {
|
|
eprintln!("Invalid spend hook: {e}");
|
|
exit(2);
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let user_data = match user_data {
|
|
Some(u) => {
|
|
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
eprintln!("Invalid user data: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
match pallas::Base::from_repr(bytes).into() {
|
|
Some(v) => Some(v),
|
|
None => {
|
|
eprintln!("Invalid user data");
|
|
exit(2);
|
|
}
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let tx = match drk
|
|
.mint_token(&amount, *rcpt.public_key(), token_id, spend_hook, user_data)
|
|
.await
|
|
{
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create token mint transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
TokenSubcmd::Freeze { token } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let token_id = match drk.get_token(token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Invalid Token ID: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let tx = match drk.freeze_token(token_id).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Failed to create token freeze transaction: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
},
|
|
|
|
Subcmd::Contract { command } => match command {
|
|
ContractSubcmd::GenerateDeploy => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let mut output = vec![];
|
|
if let Err(e) = drk.deploy_auth_keygen(&mut output).await {
|
|
print_output(&output);
|
|
eprintln!("Error creating deploy auth keypair: {e}");
|
|
exit(2);
|
|
}
|
|
print_output(&output);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ContractSubcmd::List { contract_id } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Some(contract_id) = contract_id {
|
|
let contract_id = match ContractId::from_str(&contract_id) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Invalid contract id: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let history = drk.get_deploy_auth_history(&contract_id).await?;
|
|
|
|
let table = prettytable_contract_history(&history);
|
|
|
|
if table.is_empty() {
|
|
println!("No history records found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
let auths = drk.list_deploy_auth().await?;
|
|
|
|
let table = prettytable_contract_auth(&auths);
|
|
|
|
if table.is_empty() {
|
|
println!("No deploy authorities found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ContractSubcmd::ExportData { tx_hash } => {
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let pair = drk.get_deploy_history_record_data(&tx_hash).await?;
|
|
|
|
println!("{}", base64::encode(&serialize_async(&pair).await));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => {
|
|
// Parse the deployment authority contract id
|
|
let deploy_auth = match ContractId::from_str(&deploy_auth) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Invalid deploy authority: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
// Read the wasm bincode and deploy instruction
|
|
let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?;
|
|
let deploy_ix = match deploy_ix {
|
|
Some(p) => smol::fs::read(expand_path(&p)?).await?,
|
|
None => vec![],
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let tx = match drk.deploy_contract(&deploy_auth, wasm_bin, deploy_ix).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Error creating contract deployment tx: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
ContractSubcmd::Lock { deploy_auth } => {
|
|
// Parse the deployment authority contract id
|
|
let deploy_auth = match ContractId::from_str(&deploy_auth) {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Invalid deploy authority: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
let drk = new_wallet(
|
|
network,
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
&ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let tx = match drk.lock_contract(&deploy_auth).await {
|
|
Ok(tx) => tx,
|
|
Err(e) => {
|
|
eprintln!("Error creating contract lock tx: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{}", base64::encode(&serialize_async(&tx).await));
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
},
|
|
}
|
|
}
|