use ckb_jsonrpc_types as json_types;
use ckb_sdk::{
constants::SIGHASH_TYPE_HASH,
rpc::CkbRpcClient,
traits::{
DefaultCellCollector, DefaultCellDepResolver, DefaultHeaderDepResolver,
DefaultTransactionDependencyProvider, SecpCkbRawKeySigner,
},
tx_builder::{
balance_tx_capacity, fill_placeholder_witnesses, transfer::CapacityTransferBuilder,
unlock_tx, CapacityBalancer, TxBuilder,
},
types::NetworkType,
unlock::{MultisigConfig, OmniLockUnlocker, ScriptUnlocker},
unlock::{OmniLockConfig, OmniLockScriptSigner, OmniUnlockMode},
Address, HumanCapacity, ScriptGroup, ScriptId,
};
use ckb_types::{
bytes::Bytes,
core::{BlockView, ScriptHashType, TransactionView},
packed::{Byte32, CellDep, CellOutput, OutPoint, Script, Transaction, WitnessArgs},
prelude::*,
H160, H256,
};
use clap::{Args, Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::{collections::HashMap, error::Error as StdErr};
// https://github.com/XuJiandong/rfcs/blob/omnilock/rfcs/0042-omnilock/0042-omnilock.md
// pub const OMNILOCK_TYPE_HASH: H256 =
// h256!("0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb");
/*
# examples for the developer local node
* note: all the address and sender-key are all examples, so you should not send capacity to these address.
########################### mulitsig omnilock example #################################
# 1. build a omnilock address
./target/debug/examples/transfer_from_omnilock_multisig build \
--omnilock-tx-hash 34e39e16a285d951b587e88f74286cbdb09c27a5c7e86aa1b1c92058a3cbcc52 --omnilock-index 0 \
--require-first-n 0 \
--threshold 2 \
--sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \
--sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \
--sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4
# {
# "lock-arg": "0x065d7d0128eeaa6f9656a229b42aadd0b177d387eb00",
# "lock-hash": "0xd93312782194cdb1a23dd73128795fd6a71ceb067ea9fd10546b95853d45f08e",
# "mainnet": "ckb1qqklkz85v4xt39ws5dd2hdv8xsy4jnpe3envjzvddqecxr0mgvrksqgxt47sz28w4fhev44z9x6z4twsk9ma8pltqqad8v6p",
# "testnet": "ckt1qqklkz85v4xt39ws5dd2hdv8xsy4jnpe3envjzvddqecxr0mgvrksqgxt47sz28w4fhev44z9x6z4twsk9ma8pltqqx6nqmf"
# }
# 2. transfer capacity to the address
ckb-cli wallet transfer --from-account 0xb398368a8ed39448f95479c1178ff3fc5e316318 \
--to-address ckt1qqklkz85v4xt39ws5dd2hdv8xsy4jnpe3envjzvddqecxr0mgvrksqgxt47sz28w4fhev44z9x6z4twsk9ma8pltqqx6nqmf \
--capacity 200 --tx-fee 0.001 --skip-check-to-address
# 0x2eecdfc28b58dc8af81cee8c1de03d4ed3ee9dd179cf37ea91530f84046cd21f
# 3. generate the transaction
./target/debug/examples/transfer_from_omnilock_multisig gen \
--omnilock-tx-hash 34e39e16a285d951b587e88f74286cbdb09c27a5c7e86aa1b1c92058a3cbcc52 --omnilock-index 0 \
--receiver ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug \
--capacity 120.0 \
--require-first-n 0 \
--threshold 2 \
--sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \
--sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \
--sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 \
--tx-file tx.json
# 4. sign the transaction
./target/debug/examples/transfer_from_omnilock_multisig sign \
--sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
--sender-key d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \
--omnilock-tx-hash 34e39e16a285d951b587e88f74286cbdb09c27a5c7e86aa1b1c92058a3cbcc52 --omnilock-index 0 \
--tx-file tx.json
# 5. send transaction
./target/debug/examples/transfer_from_omnilock send --tx-file tx.json
*/
#[derive(Args)]
struct BuildOmniLockAddrArgs {
/// 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
,
/// omnilock script deploy transaction hash
#[clap(long, value_name = "H256")]
omnilock_tx_hash: H256,
/// cell index of omnilock script deploy transaction's outputs
#[clap(long, value_name = "NUMBER")]
omnilock_index: usize,
/// CKB rpc url
#[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")]
ckb_rpc: String,
}
#[derive(Args)]
struct GenTxArgs {
/// 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 receiver address
#[clap(long, value_name = "ADDRESS")]
receiver: Address,
/// omnilock script deploy transaction hash
#[clap(long, value_name = "H256")]
omnilock_tx_hash: H256,
/// cell index of omnilock script deploy transaction's outputs
#[clap(long, value_name = "NUMBER")]
omnilock_index: usize,
/// The capacity to transfer (unit: CKB, example: 102.43)
#[clap(long, value_name = "CKB")]
capacity: HumanCapacity,
/// 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 key (hex string)
#[clap(long, value_name = "KEY")]
sender_key: Vec,
/// The output transaction info file (.json)
#[clap(long, value_name = "PATH")]
tx_file: PathBuf,
/// omnilock script deploy transaction hash
#[clap(long, value_name = "H256")]
omnilock_tx_hash: H256,
/// cell index of omnilock script deploy transaction's outputs
#[clap(long, value_name = "NUMBER")]
omnilock_index: usize,
/// CKB rpc url
#[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")]
ckb_rpc: String,
}
#[derive(Subcommand)]
enum Commands {
/// build omni lock address
Build(BuildOmniLockAddrArgs),
/// 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(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Serialize, Deserialize)]
struct TxInfo {
tx: json_types::TransactionView,
omnilock_config: OmniLockConfig,
}
struct OmniLockInfo {
type_hash: H256,
script_id: ScriptId,
cell_dep: CellDep,
}
fn main() -> Result<(), Box> {
// Parse arguments
let cli = Cli::parse();
match cli.command {
Commands::Build(build_args) => build_omnilock_addr(&build_args)?,
Commands::Gen(gen_args) => {
gen_omnilock_tx(&gen_args)?;
}
Commands::Sign(args) => {
let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?;
let tx = Transaction::from(tx_info.tx.inner).into_view();
let keys: Vec<_> = args
.sender_key
.iter()
.map(|sender_key| {
secp256k1::SecretKey::from_slice(sender_key.as_bytes())
.map_err(|err| format!("invalid sender secret key: {}", err))
.unwrap()
})
.collect();
let (tx, _) = sign_tx(&args, tx, &tx_info.omnilock_config, keys)?;
let witness_args =
WitnessArgs::from_slice(tx.witnesses().get(0).unwrap().raw_data().as_ref())?;
let lock_field = witness_args.lock().to_opt().unwrap().raw_data();
if lock_field != tx_info.omnilock_config.zero_lock(OmniUnlockMode::Normal)? {
println!("> transaction ready to send!");
} else {
println!("failed to sign tx");
}
let tx_info = TxInfo {
tx: json_types::TransactionView::from(tx),
omnilock_config: tx_info.omnilock_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)?);
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_multisig_config(
sighash_address: &[Address],
require_first_n: u8,
threshold: u8,
) -> Result> {
if sighash_address.is_empty() {
return Err("Must have at least one sighash_address".to_string().into());
}
let mut sighash_addresses = Vec::with_capacity(sighash_address.len());
for addr in sighash_address {
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());
}
Ok(MultisigConfig::new_with(
sighash_addresses,
require_first_n,
threshold,
)?)
}
fn build_omnilock_addr(args: &BuildOmniLockAddrArgs) -> Result<(), Box> {
let ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
let cell = build_omnilock_cell_dep(&ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?;
let multisig_config =
build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?;
let config = OmniLockConfig::new_multisig(multisig_config);
let address_payload = {
let args = config.build_args();
ckb_sdk::AddressPayload::new_full(ScriptHashType::Type, cell.type_hash.pack(), args)
};
let lock_script = Script::from(&address_payload);
let resp = serde_json::json!({
"mainnet": Address::new(NetworkType::Mainnet, address_payload.clone(), true).to_string(),
"testnet": Address::new(NetworkType::Testnet, address_payload.clone(), true).to_string(),
"lock-arg": format!("0x{}", hex_string(address_payload.args().as_ref())),
"lock-hash": format!("{:#x}", lock_script.calc_script_hash())
});
println!("{}", serde_json::to_string_pretty(&resp)?);
Ok(())
}
fn gen_omnilock_tx(args: &GenTxArgs) -> Result<(), Box> {
let (tx, omnilock_config) = build_transfer_tx(args)?;
let tx_info = TxInfo {
tx: json_types::TransactionView::from(tx),
omnilock_config,
};
fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?;
Ok(())
}
fn build_transfer_tx(
args: &GenTxArgs,
) -> Result<(TransactionView, OmniLockConfig), Box> {
let multisig_config =
build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?;
let ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
let cell = build_omnilock_cell_dep(&ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?;
let omnilock_config = OmniLockConfig::new_multisig(multisig_config);
// Build CapacityBalancer
let sender = Script::new_builder()
.code_hash(cell.type_hash.pack())
.hash_type(ScriptHashType::Type.into())
.args(omnilock_config.build_args().pack())
.build();
let placeholder_witness = omnilock_config.placeholder_witness(OmniUnlockMode::Normal)?;
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 genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap();
let genesis_block = BlockView::from(genesis_block);
let mut cell_dep_resolver = DefaultCellDepResolver::from_genesis(&genesis_block)?;
cell_dep_resolver.insert(cell.script_id, cell.cell_dep, "Omni Lock".to_string());
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_omnilock_unlockers(Vec::new(), omnilock_config.clone(), cell.type_hash);
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 base_tx = builder.build_base(
&mut cell_collector,
&cell_dep_resolver,
&header_dep_resolver,
&tx_dep_provider,
)?;
let secp256k1_data_dep = {
// pub const SECP256K1_DATA_OUTPUT_LOC: (usize, usize) = (0, 3);
let tx_hash = genesis_block.transactions()[0].hash();
let out_point = OutPoint::new(tx_hash, 3u32);
CellDep::new_builder().out_point(out_point).build()
};
let base_tx = base_tx
.as_advanced_builder()
.cell_dep(secp256k1_data_dep)
.build();
let (tx_filled_witnesses, _) =
fill_placeholder_witnesses(base_tx, &tx_dep_provider, &unlockers)?;
let tx = balance_tx_capacity(
&tx_filled_witnesses,
&balancer,
&mut cell_collector,
&tx_dep_provider,
&cell_dep_resolver,
&header_dep_resolver,
)?;
Ok((tx, omnilock_config))
}
fn build_omnilock_cell_dep(
ckb_client: &CkbRpcClient,
tx_hash: &H256,
index: usize,
) -> Result> {
let out_point_json = ckb_jsonrpc_types::OutPoint {
tx_hash: tx_hash.clone(),
index: ckb_jsonrpc_types::Uint32::from(index as u32),
};
let cell_status = ckb_client.get_live_cell(out_point_json, false)?;
let script = Script::from(cell_status.cell.unwrap().output.type_.unwrap());
let type_hash = script.calc_script_hash();
let out_point = OutPoint::new(Byte32::from_slice(tx_hash.as_bytes())?, index as u32);
let cell_dep = CellDep::new_builder().out_point(out_point).build();
Ok(OmniLockInfo {
type_hash: H256::from_slice(type_hash.as_slice())?,
script_id: ScriptId::new_type(type_hash.unpack()),
cell_dep,
})
}
fn build_omnilock_unlockers(
keys: Vec,
config: OmniLockConfig,
omni_lock_type_hash: H256,
) -> HashMap> {
let signer = SecpCkbRawKeySigner::new_with_secret_keys(keys);
let omnilock_signer =
OmniLockScriptSigner::new(Box::new(signer), config.clone(), OmniUnlockMode::Normal);
let omnilock_unlocker = OmniLockUnlocker::new(omnilock_signer, config);
let omnilock_script_id = ScriptId::new_type(omni_lock_type_hash);
HashMap::from([(
omnilock_script_id,
Box::new(omnilock_unlocker) as Box,
)])
}
fn sign_tx(
args: &SignTxArgs,
mut tx: TransactionView,
omnilock_config: &OmniLockConfig,
keys: Vec,
) -> Result<(TransactionView, Vec), Box> {
// Unlock transaction
let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10);
let ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
let cell = build_omnilock_cell_dep(&ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?;
let mut _still_locked_groups = None;
let unlockers = build_omnilock_unlockers(keys, omnilock_config.clone(), cell.type_hash);
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()))
}