mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-10 07:08:05 -05:00
2663 lines
87 KiB
Rust
2663 lines
87 KiB
Rust
/* This file is part of DarkFi (https://dark.fi)
|
|
*
|
|
* Copyright (C) 2020-2025 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,
|
|
sync::Arc,
|
|
};
|
|
|
|
use log::info;
|
|
use prettytable::{format, row, Table};
|
|
use rand::rngs::OsRng;
|
|
use smol::{fs::read_to_string, stream::StreamExt};
|
|
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
|
|
use url::Url;
|
|
|
|
use darkfi::{
|
|
async_daemonize, cli_desc,
|
|
util::{
|
|
encoding::base64,
|
|
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::{
|
|
note::AeadEncryptedNote, BaseBlind, FuncId, FuncRef, Keypair, PublicKey, SecretKey,
|
|
DAO_CONTRACT_ID,
|
|
},
|
|
pasta::{group::ff::PrimeField, pallas},
|
|
tx::TransactionHash,
|
|
};
|
|
use darkfi_serial::{deserialize_async, serialize_async};
|
|
|
|
use drk::{
|
|
cli_util::{
|
|
generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair,
|
|
},
|
|
dao::{DaoParams, ProposalRecord},
|
|
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()
|
|
#[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 {
|
|
/// 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(long)]
|
|
/// Initialize wallet database
|
|
initialize: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Generate a new keypair in the wallet
|
|
keygen: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Query the wallet for known balances
|
|
balance: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Get the default address in the wallet
|
|
address: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Print all the addresses in the wallet
|
|
addresses: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Set the default address in the wallet
|
|
default_address: Option<usize>,
|
|
|
|
#[structopt(long)]
|
|
/// Print all the secret keys from the wallet
|
|
secrets: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Import secret keys from stdin into the wallet, separated by newlines
|
|
import_secrets: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Print the Merkle tree in the wallet
|
|
tree: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Print all the coins in the wallet
|
|
coins: bool,
|
|
},
|
|
|
|
/// Read a transaction from stdin and mark its input coins as spent
|
|
Spend,
|
|
|
|
/// Unspend a coin
|
|
Unspend {
|
|
/// base58-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,
|
|
},
|
|
|
|
/// Attach the fee call to a transaction given from stdin
|
|
AttachFee,
|
|
|
|
/// Inspect a transaction from stdin
|
|
Inspect,
|
|
|
|
/// Read a transaction from stdin and broadcast it
|
|
Broadcast,
|
|
|
|
/// This subscription will listen for incoming blocks from darkfid and look
|
|
/// through their transactions to see if there's any that interest us.
|
|
/// With `drk` we look at transactions calling the money contract so we can
|
|
/// find coins sent to us and fill our wallet with the necessary metadata.
|
|
Subscribe,
|
|
|
|
/// DAO functionalities
|
|
Dao {
|
|
#[structopt(subcommand)]
|
|
/// Sub command to execute
|
|
command: DaoSubcmd,
|
|
},
|
|
|
|
/// 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 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,
|
|
},
|
|
|
|
/// Update DAO keys from stdin
|
|
UpdateKeys,
|
|
|
|
/// 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 base58-encoded spend hook
|
|
SpendHook,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, StructOpt)]
|
|
enum ExplorerSubcmd {
|
|
/// Fetch a blockchain transaction by hash
|
|
FetchTx {
|
|
/// Transaction hash
|
|
tx_hash: String,
|
|
|
|
#[structopt(long)]
|
|
/// Print the full transaction information
|
|
full: bool,
|
|
|
|
#[structopt(long)]
|
|
/// Encode transaction to base58
|
|
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 base58
|
|
encode: bool,
|
|
},
|
|
|
|
/// Remove reverted transactions from history
|
|
ClearReverted,
|
|
|
|
/// Fetch scanned blocks records
|
|
ScannedBlocks {
|
|
/// Fetch specific height record (optional)
|
|
height: Option<u32>,
|
|
},
|
|
}
|
|
|
|
#[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
|
|
List,
|
|
|
|
/// Deploy a smart contract
|
|
Deploy {
|
|
/// Contract ID (deploy authority)
|
|
deploy_auth: u64,
|
|
|
|
/// Path to contract wasm bincode
|
|
wasm_path: String,
|
|
|
|
/// Path to serialized deploy instruction
|
|
deploy_ix: String,
|
|
},
|
|
|
|
/// Lock a smart contract
|
|
Lock {
|
|
/// Contract ID (deploy authority)
|
|
deploy_auth: u64,
|
|
},
|
|
}
|
|
|
|
/// 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:8240")]
|
|
/// darkfid JSON-RPC endpoint
|
|
endpoint: Url,
|
|
}
|
|
|
|
/// Auxiliary function to parse darkfid configuration file and extract requested
|
|
/// blockchain network config.
|
|
async fn parse_blockchain_config(
|
|
config: Option<String>,
|
|
network: &str,
|
|
) -> Result<BlockchainNetwork> {
|
|
// 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(network_config)
|
|
}
|
|
|
|
/// Auxiliary function to create a `Drk` wallet for provided configuration.
|
|
async fn new_wallet(
|
|
cache_path: String,
|
|
wallet_path: String,
|
|
wallet_pass: String,
|
|
endpoint: Option<Url>,
|
|
ex: Arc<smol::Executor<'static>>,
|
|
fun: bool,
|
|
) -> Drk {
|
|
// Script kiddies protection
|
|
if wallet_pass == "changeme" {
|
|
eprintln!("Please don't use default wallet password...");
|
|
exit(2);
|
|
}
|
|
|
|
match Drk::new(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: Arc<smol::Executor<'static>>) -> Result<()> {
|
|
// Grab blockchain network configuration
|
|
let 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::Kaching => {
|
|
if !args.fun {
|
|
println!("Apparently you don't like fun...");
|
|
return Ok(())
|
|
}
|
|
kaching().await;
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Ping => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
drk.ping().await?;
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Completions { shell } => generate_completions(&shell),
|
|
|
|
Subcmd::Wallet {
|
|
initialize,
|
|
keygen,
|
|
balance,
|
|
address,
|
|
addresses,
|
|
default_address,
|
|
secrets,
|
|
import_secrets,
|
|
tree,
|
|
coins,
|
|
} => {
|
|
if !initialize &&
|
|
!keygen &&
|
|
!balance &&
|
|
!address &&
|
|
!addresses &&
|
|
default_address.is_none() &&
|
|
!secrets &&
|
|
!tree &&
|
|
!coins &&
|
|
!import_secrets
|
|
{
|
|
eprintln!("Error: You must use at least one flag for this subcommand");
|
|
eprintln!("Run with \"wallet -h\" to see the subcommand usage.");
|
|
exit(2);
|
|
}
|
|
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if initialize {
|
|
if let Err(e) = drk.initialize_wallet().await {
|
|
eprintln!("Error initializing wallet: {e:?}");
|
|
exit(2);
|
|
}
|
|
if let Err(e) = drk.initialize_money().await {
|
|
eprintln!("Failed to initialize Money: {e:?}");
|
|
exit(2);
|
|
}
|
|
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);
|
|
}
|
|
return Ok(())
|
|
}
|
|
|
|
if keygen {
|
|
if let Err(e) = drk.money_keygen().await {
|
|
eprintln!("Failed to generate keypair: {e:?}");
|
|
exit(2);
|
|
}
|
|
return Ok(())
|
|
}
|
|
|
|
if 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}");
|
|
}
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if address {
|
|
let address = match drk.default_address().await {
|
|
Ok(a) => a,
|
|
Err(e) => {
|
|
eprintln!("Failed to fetch default address: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("{address}");
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if addresses {
|
|
let addresses = drk.addresses().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!["Key ID", "Public Key", "Secret Key", "Is Default"]);
|
|
for (key_id, public_key, secret_key, is_default) in addresses {
|
|
let is_default = match is_default {
|
|
1 => "*",
|
|
_ => "",
|
|
};
|
|
table.add_row(row![key_id, public_key, secret_key, is_default]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No addresses found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if let Some(idx) = default_address {
|
|
if let Err(e) = drk.set_default_address(idx) {
|
|
eprintln!("Failed to set default address: {e:?}");
|
|
exit(2);
|
|
}
|
|
return Ok(())
|
|
}
|
|
|
|
if secrets {
|
|
let v = drk.get_money_secrets().await?;
|
|
|
|
for i in v {
|
|
println!("{i}");
|
|
}
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if import_secrets {
|
|
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 pubkeys = match drk.import_money_secrets(secrets).await {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
eprintln!("Failed to import secret keys into wallet: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
for key in pubkeys {
|
|
println!("{key}");
|
|
}
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if tree {
|
|
let tree = drk.get_money_tree().await?;
|
|
|
|
println!("{tree:#?}");
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
if coins {
|
|
let coins = drk.get_coins(true).await?;
|
|
|
|
let aliases_map = drk.get_aliases_mapped_by_token().await?;
|
|
|
|
if coins.is_empty() {
|
|
return Ok(())
|
|
}
|
|
|
|
let mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row![
|
|
"Coin",
|
|
"Spent",
|
|
"Token ID",
|
|
"Aliases",
|
|
"Value",
|
|
"Spend Hook",
|
|
"User Data",
|
|
"Spent TX"
|
|
]);
|
|
for coin in coins {
|
|
let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) {
|
|
Some(a) => a,
|
|
None => "-",
|
|
};
|
|
|
|
let spend_hook = if coin.0.note.spend_hook != FuncId::none() {
|
|
format!("{}", coin.0.note.spend_hook)
|
|
} else {
|
|
String::from("-")
|
|
};
|
|
|
|
let user_data = if coin.0.note.user_data != pallas::Base::ZERO {
|
|
bs58::encode(&serialize_async(&coin.0.note.user_data).await)
|
|
.into_string()
|
|
.to_string()
|
|
} else {
|
|
String::from("-")
|
|
};
|
|
|
|
table.add_row(row![
|
|
bs58::encode(&serialize_async(&coin.0.coin.inner()).await)
|
|
.into_string()
|
|
.to_string(),
|
|
coin.1,
|
|
coin.0.note.token_id,
|
|
aliases,
|
|
format!(
|
|
"{} ({})",
|
|
coin.0.note.value,
|
|
encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS)
|
|
),
|
|
spend_hook,
|
|
user_data,
|
|
coin.2
|
|
]);
|
|
}
|
|
|
|
println!("{table}");
|
|
|
|
return Ok(())
|
|
}
|
|
|
|
unreachable!()
|
|
}
|
|
|
|
Subcmd::Spend => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = drk.mark_tx_spend(&tx).await {
|
|
eprintln!("Failed to mark transaction coins as spent: {e:?}");
|
|
exit(2);
|
|
};
|
|
|
|
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(
|
|
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(
|
|
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 PublicKey::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e:?}");
|
|
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, 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(
|
|
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(
|
|
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(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.inspect_swap(bytes).await {
|
|
eprintln!("Failed to inspect swap: {e:?}");
|
|
exit(2);
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
OtcSubcmd::Sign => {
|
|
let mut tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
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(
|
|
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(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.import_dao(&name, ¶ms).await {
|
|
eprintln!("Failed to import DAO: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::UpdateKeys => {
|
|
let mut buf = String::new();
|
|
stdin().read_to_string(&mut buf)?;
|
|
let params = DaoParams::from_toml_str(&buf)?;
|
|
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.update_dao_keys(¶ms).await {
|
|
eprintln!("Failed to update DAO keys: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::List { name } => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.dao_list(&name).await {
|
|
eprintln!("Failed to list DAO: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Balance { name } => {
|
|
let drk = new_wallet(
|
|
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);
|
|
}
|
|
};
|
|
|
|
// 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}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
DaoSubcmd::Mint { name } => {
|
|
let drk = new_wallet(
|
|
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(
|
|
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 PublicKey::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e:?}");
|
|
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, 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(
|
|
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 transfer proposal: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Generated proposal: {}", proposal.bulla());
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
DaoSubcmd::Proposals { name } => {
|
|
let drk = new_wallet(
|
|
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(
|
|
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 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",
|
|
coin.public_key,
|
|
"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(
|
|
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(
|
|
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(
|
|
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(())
|
|
}
|
|
},
|
|
|
|
Subcmd::AttachFee => {
|
|
let mut tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
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::Inspect => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
println!("{tx:#?}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
Subcmd::Broadcast => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
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);
|
|
};
|
|
|
|
if let Err(e) = drk.mark_tx_spend(&tx).await {
|
|
eprintln!("Failed to mark transaction coins as spent: {e:?}");
|
|
exit(2);
|
|
};
|
|
|
|
let txid = match drk.broadcast_tx(&tx).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
eprintln!("Failed to broadcast transaction: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Transaction ID: {txid}");
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Subscribe => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint.clone()),
|
|
ex.clone(),
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = drk.subscribe_blocks(blockchain_config.endpoint, ex).await {
|
|
eprintln!("Block subscription failed: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
Subcmd::Scan { reset } => {
|
|
let drk = new_wallet(
|
|
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 {
|
|
if let Err(e) = drk.reset_to_height(height).await {
|
|
eprintln!("Failed during wallet reset: {e:?}");
|
|
exit(2);
|
|
}
|
|
}
|
|
|
|
if let Err(e) = drk.scan_blocks().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, full, encode } => {
|
|
let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes());
|
|
|
|
let drk = new_wallet(
|
|
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}");
|
|
if full {
|
|
println!("{tx:?}");
|
|
}
|
|
|
|
drk.stop_rpc_client().await
|
|
}
|
|
|
|
ExplorerSubcmd::SimulateTx => {
|
|
let tx = parse_tx_from_stdin().await?;
|
|
|
|
let drk = new_wallet(
|
|
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(
|
|
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(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = drk.remove_reverted_txs() {
|
|
eprintln!("Failed to remove reverted transactions: {e:?}");
|
|
exit(2);
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ExplorerSubcmd::ScannedBlocks { height } => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Some(height) = height {
|
|
let hash = match drk.get_scanned_block_hash(&height) {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
eprintln!("Failed to retrieve scanned block record: {e:?}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
println!("Height: {height}");
|
|
println!("Hash: {hash}");
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
// 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!["Height", "Hash"]);
|
|
for (height, hash) in map.iter() {
|
|
table.add_row(row![height, hash]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No scanned blocks records found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
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(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.add_alias(alias, token_id).await {
|
|
eprintln!("Failed to add alias: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
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(
|
|
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?;
|
|
|
|
// 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!["Alias", "Token ID"]);
|
|
for (alias, token_id) in map.iter() {
|
|
table.add_row(row![alias, token_id]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No aliases found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
AliasSubcmd::Remove { alias } => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
if let Err(e) = drk.remove_alias(alias).await {
|
|
eprintln!("Failed to remove alias: {e:?}");
|
|
exit(2);
|
|
}
|
|
|
|
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(
|
|
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(
|
|
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(
|
|
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 mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row![
|
|
"Token ID",
|
|
"Aliases",
|
|
"Mint Authority",
|
|
"Token Blind",
|
|
"Frozen",
|
|
"Freeze Height"
|
|
]);
|
|
|
|
for (token_id, authority, blind, frozen, freeze_height) in tokens {
|
|
let aliases = match aliases_map.get(&token_id.to_string()) {
|
|
Some(a) => a,
|
|
None => "-",
|
|
};
|
|
|
|
let freeze_height = match freeze_height {
|
|
Some(freeze_height) => freeze_height.to_string(),
|
|
None => String::from("-"),
|
|
};
|
|
|
|
table.add_row(row![token_id, aliases, authority, blind, frozen, freeze_height]);
|
|
}
|
|
|
|
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(
|
|
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 PublicKey::from_str(&recipient) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("Invalid recipient: {e:?}");
|
|
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, 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(
|
|
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(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
if let Err(e) = drk.deploy_auth_keygen().await {
|
|
eprintln!("Error creating deploy auth keypair: {e}");
|
|
exit(2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ContractSubcmd::List => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
None,
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
let auths = drk.list_deploy_auth().await?;
|
|
|
|
let mut table = Table::new();
|
|
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
|
table.set_titles(row!["Index", "Contract ID", "Frozen", "Freeze Height"]);
|
|
|
|
for (idx, contract_id, frozen, freeze_height) in auths {
|
|
let freeze_height = match freeze_height {
|
|
Some(freeze_height) => freeze_height.to_string(),
|
|
None => String::from("-"),
|
|
};
|
|
table.add_row(row![idx, contract_id, frozen, freeze_height]);
|
|
}
|
|
|
|
if table.is_empty() {
|
|
println!("No deploy authorities found");
|
|
} else {
|
|
println!("{table}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => {
|
|
// Read the wasm bincode and deploy instruction
|
|
let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?;
|
|
let deploy_ix = smol::fs::read(expand_path(&deploy_ix)?).await?;
|
|
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let mut tx = match drk.deploy_contract(deploy_auth, wasm_bin, deploy_ix).await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Error creating contract deployment tx: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
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
|
|
}
|
|
|
|
ContractSubcmd::Lock { deploy_auth } => {
|
|
let drk = new_wallet(
|
|
blockchain_config.cache_path,
|
|
blockchain_config.wallet_path,
|
|
blockchain_config.wallet_pass,
|
|
Some(blockchain_config.endpoint),
|
|
ex,
|
|
args.fun,
|
|
)
|
|
.await;
|
|
|
|
let mut tx = match drk.lock_contract(deploy_auth).await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Error creating contract lock tx: {e}");
|
|
exit(2);
|
|
}
|
|
};
|
|
|
|
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
|
|
}
|
|
},
|
|
}
|
|
}
|