use std::collections::HashMap;
use std::error::Error as StdErr;
use std::fs;
use std::path::PathBuf;
use ckb_hash::blake2b_256;
use ckb_jsonrpc_types as json_types;
use ckb_sdk::{
constants::{MULTISIG_TYPE_HASH, SIGHASH_TYPE_HASH},
rpc::CkbRpcClient,
traits::{
DefaultCellCollector, DefaultCellDepResolver, DefaultHeaderDepResolver,
DefaultTransactionDependencyProvider, SecpCkbRawKeySigner,
},
tx_builder::{transfer::CapacityTransferBuilder, unlock_tx, CapacityBalancer, TxBuilder},
unlock::{MultisigConfig, ScriptUnlocker, SecpMultisigScriptSigner, SecpMultisigUnlocker},
Address, AddressPayload, HumanCapacity, NetworkType, ScriptGroup, ScriptId, SECP256K1,
};
use ckb_types::{
bytes::Bytes,
core::{BlockView, ScriptHashType, TransactionView},
packed::{CellOutput, Script, Transaction, WitnessArgs},
prelude::*,
H160, H256,
};
use clap::{Args, Parser, Subcommand};
use serde::{Deserialize, Serialize};
/// Transfer some CKB from one multisig(without since) address to other address
/// # Example:
/// ./target/debug/examples/transfer_from_multisig gen \
/// --receiver
\
/// --capacity 120.0 \
/// --require-first-n 0 \
/// --threshold 2 \
/// --sighash-address \
/// --sighash-address \
/// --sighash-address \
/// --tx-file tx.json
///
/// ./target/debug/examples/transfer_from_multisig sign \
/// --sender-key \
/// --tx-file tx.json
///
/// ./target/debug/examples/transfer_from_multisig send --tx-file tx.json
///
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Args)]
struct GenTxArgs {
/// The receiver address
#[clap(long, value_name = "ADDRESS")]
receiver: Address,
/// The capacity to transfer (unit: CKB, example: 102.43)
#[clap(long, value_name = "CKB")]
capacity: HumanCapacity,
/// Require first n signatures of corresponding pubkey
#[clap(long, value_name = "NUM")]
require_first_n: u8,
/// Multisig threshold
#[clap(long, value_name = "NUM")]
threshold: u8,
/// Normal sighash address
#[clap(long, value_name = "ADDRESS")]
sighash_address: Vec,
/// The output transaction info file (.json)
#[clap(long, value_name = "PATH")]
tx_file: PathBuf,
/// CKB rpc url
#[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")]
ckb_rpc: String,
}
#[derive(Args)]
struct SignTxArgs {
/// The sender private keys (hex string, must presented in `sighash_address`)
#[clap(long, value_name = "KEY")]
sender_key: Vec,
/// The transaction info file (.json)
#[clap(long, value_name = "PATH")]
tx_file: PathBuf,
/// CKB rpc url
#[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")]
ckb_rpc: String,
}
#[derive(Subcommand)]
enum Commands {
/// Generate the transaction
Gen(GenTxArgs),
/// Sign the transaction
Sign(SignTxArgs),
/// Send the transaction
Send {
/// The transaction info file (.json)
#[clap(long, value_name = "PATH")]
tx_file: PathBuf,
/// CKB rpc url
#[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")]
ckb_rpc: String,
},
}
#[derive(Serialize, Deserialize)]
struct TxInfo {
tx: json_types::TransactionView,
multisig_config: MultisigConfig,
}
fn main() -> Result<(), Box> {
// Parse arguments
let cli = Cli::parse();
match cli.command {
Commands::Gen(args) => {
let multisig_config = {
if args.sighash_address.is_empty() {
return Err("Must have at least one sighash_address".to_string().into());
}
let mut sighash_addresses = Vec::with_capacity(args.sighash_address.len());
for addr in args.sighash_address.clone() {
let lock_args = addr.payload().args();
if addr.payload().code_hash(None).as_slice() != SIGHASH_TYPE_HASH.as_bytes()
|| addr.payload().hash_type() != ScriptHashType::Type
|| lock_args.len() != 20
{
return Err(
format!("sighash_address {} is not sighash address", addr).into()
);
}
sighash_addresses.push(H160::from_slice(lock_args.as_ref()).unwrap());
}
MultisigConfig::new_with(sighash_addresses, args.require_first_n, args.threshold)?
};
let tx = build_transfer_tx(&args, &multisig_config)?;
let tx_info = TxInfo {
tx: json_types::TransactionView::from(tx),
multisig_config,
};
fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?;
}
Commands::Sign(args) => {
if args.sender_key.is_empty() {
return Err("sender key is missing".to_string().into());
}
let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?;
let mut sender_keys = Vec::with_capacity(args.sender_key.len());
for key_bin in args.sender_key.clone() {
let key = secp256k1::SecretKey::from_slice(key_bin.as_bytes())
.map_err(|err| format!("invalid sender secret key: {}", err))?;
let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &key);
let hash160 =
H160::from_slice(&blake2b_256(&pubkey.serialize()[..])[0..20]).unwrap();
if !tx_info.multisig_config.contains_address(&hash160) {
return Err(format!("key {:#x} is not in multisig config", key_bin).into());
}
sender_keys.push(key);
let address = Address::new(
NetworkType::Testnet,
AddressPayload::from_pubkey_hash(hash160),
true,
);
println!("> sign by address(testnet): {}", address);
}
let tx = Transaction::from(tx_info.tx.inner).into_view();
let (tx, _) = sign_tx(&args, tx, &tx_info.multisig_config, sender_keys)?;
let config_data_len = tx_info.multisig_config.to_witness_data().len();
let lock_field =
WitnessArgs::from_slice(tx.witnesses().get(0).unwrap().raw_data().as_ref())?
.lock()
.to_opt()
.unwrap()
.raw_data();
if (0..tx_info.multisig_config.threshold() as usize).all(|i| {
lock_field.as_ref()[config_data_len + i * 65..config_data_len + (i + 1) * 65]
!= [0u8; 65]
}) {
println!("> transaction ready to send!");
} else {
println!("> need more keys to sign the transaction!");
}
let tx_info = TxInfo {
tx: json_types::TransactionView::from(tx),
multisig_config: tx_info.multisig_config,
};
fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?;
}
Commands::Send { tx_file, ckb_rpc } => {
// Send transaction
let tx_info: TxInfo = serde_json::from_slice(&fs::read(tx_file)?)?;
println!(
"> tx: {}",
serde_json::to_string_pretty(&tx_info.tx).unwrap()
);
let outputs_validator = Some(json_types::OutputsValidator::Passthrough);
let _tx_hash = CkbRpcClient::new(ckb_rpc.as_str())
.send_transaction(tx_info.tx.inner, outputs_validator)
.expect("send transaction");
println!(">>> tx sent! <<<");
}
}
Ok(())
}
fn build_transfer_tx(
args: &GenTxArgs,
multisig_config: &MultisigConfig,
) -> Result> {
// Build CapacityBalancer
let sender = Script::new_builder()
.code_hash(MULTISIG_TYPE_HASH.pack())
.hash_type(ScriptHashType::Type.into())
.args(Bytes::from(multisig_config.hash160().as_bytes().to_vec()).pack())
.build();
let sender_addr = Address::new(args.receiver.network(), sender.clone().into(), true);
println!("> sender address: {}", sender_addr);
let placeholder_witness = multisig_config.placeholder_witness();
let balancer = CapacityBalancer::new_simple(sender, placeholder_witness, 1000);
// Build:
// * CellDepResolver
// * HeaderDepResolver
// * CellCollector
// * TransactionDependencyProvider
let ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
let cell_dep_resolver = {
let genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap();
DefaultCellDepResolver::from_genesis(&BlockView::from(genesis_block))?
};
let header_dep_resolver = DefaultHeaderDepResolver::new(args.ckb_rpc.as_str());
let mut cell_collector = DefaultCellCollector::new(args.ckb_rpc.as_str());
let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10);
// Build base transaction
let unlockers = build_multisig_unlockers(Vec::new(), multisig_config.clone());
let output = CellOutput::new_builder()
.lock(Script::from(&args.receiver))
.capacity(args.capacity.0.pack())
.build();
let builder = CapacityTransferBuilder::new(vec![(output, Bytes::default())]);
let tx = builder.build_balanced(
&mut cell_collector,
&cell_dep_resolver,
&header_dep_resolver,
&tx_dep_provider,
&balancer,
&unlockers,
)?;
Ok(tx)
}
fn sign_tx(
args: &SignTxArgs,
mut tx: TransactionView,
multisig_config: &MultisigConfig,
sender_keys: Vec,
) -> Result<(TransactionView, Vec), Box> {
// Unlock transaction
let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10);
let mut still_locked_groups = None;
for key in sender_keys {
let unlockers = build_multisig_unlockers(vec![key], multisig_config.clone());
let (new_tx, new_still_locked_groups) =
unlock_tx(tx.clone(), &tx_dep_provider, &unlockers)?;
tx = new_tx;
still_locked_groups = Some(new_still_locked_groups);
}
Ok((tx, still_locked_groups.unwrap_or_default()))
}
fn build_multisig_unlockers(
keys: Vec,
config: MultisigConfig,
) -> HashMap> {
let signer = SecpCkbRawKeySigner::new_with_secret_keys(keys);
let multisig_signer = SecpMultisigScriptSigner::new(Box::new(signer), config);
let multisig_unlocker = SecpMultisigUnlocker::new(multisig_signer);
let multisig_script_id = ScriptId::new_type(MULTISIG_TYPE_HASH.clone());
let mut unlockers = HashMap::default();
unlockers.insert(
multisig_script_id,
Box::new(multisig_unlocker) as Box,
);
unlockers
}