diff --git a/CHANGELOG.md b/CHANGELOG.md
index d02abd01..cd92e77a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
+# 2.4.0
+* **BREAKING CHANGE**: add parameter `tx_dep_provider: &dyn TransactionDependencyProvider` to `ScriptSigner::sing_tx`.
+* Support open transaction.
+
# 2.3.0
* Update ckb to v0.105.1
* **BREAKING CHANGE**: `get_transaction` rpc now return `TransactionWithStatusResponse`
diff --git a/examples/transfer_from_opentx.md b/examples/transfer_from_opentx.md
new file mode 100644
index 00000000..b076b231
--- /dev/null
+++ b/examples/transfer_from_opentx.md
@@ -0,0 +1,250 @@
+
+This document is about how to use the transfer_from_opentx example to do open transaction operation.
+All the addresses and keys are all in my development local node, you should not use in the production environment.
+# Sighash open transaction example
+1. Build an opentx address
+```bash
+ ./target/debug/examples/transfer_from_opentx build --receiver ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024
+ ```
+ The output:
+ ```json
+{
+ "lock-arg": "0x00b398368a8ed39448f95479c1178ff3fc5e31631810",
+ "lock-hash": "0x3f54ccaf46b3472b55eaa2e2c0a5cae87575b3de90a81fe60206dd5c0951ffa8",
+ "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqv8f7ak",
+ "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7"
+}
+```
+2. Transfer capacity to the address
+```bash
+ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \
+ --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 99 --skip-check-to-address
+# 0x937deeb989bbd7f4bd0273bf2049d7614615dd58a32090b0093f23a692715871
+```
+3. Generate the transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 98.0 --open-capacity 1.0\
+ --tx-file tx.json
+```
+4. Sign the transaction
+```bash
+./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --tx-file tx.json
+```
+5. Add input, with capacity 98.99999588
+```bash
+./target/debug/examples/transfer_from_opentx add-input --tx-hash df85d2aaa44d50b1db286bdb2fbd8682cad12d6858b269d2531403ba5e63a2eb --index 0 --tx-file tx.json
+```
+6. Add output, capacity is 98.99999588(original) + 1(open capacity) - 0.001(fee)
+```bash
+./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 99.99899588 --tx-file tx.json
+```
+7. Sign the new input
+```bash
+./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json
+```
+8. send the tx
+```bash
+./target/debug/examples/transfer_from_opentx send --tx-file tx.json
+# 0xebb9d9ff39efbee5957d6f7d19a4a17f1ac2e69dbc9289e4931cef6b832f4d57
+```
+
+# Ethereum open transaction example
+1. build an opentx address
+```bash
+./target/debug/examples/transfer_from_opentx build --ethereum-receiver 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d
+```
+output:
+```json
+{
+ "lock-arg": "0x01cf2485c76aff1f2b4464edf04a1c8045068cf7e010",
+ "lock-hash": "0x057dcd204f26621ef49346ed77d2bdbf3069b83a5ef0a2b52be5299a93507cf6",
+ "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqjzc5z5",
+ "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqf4vcru"
+}
+```
+2. Transfer capacity to the address
+```bash
+ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \
+ --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqf4vcru \
+ --capacity 99 --skip-check-to-address
+# 0xbd696b87629dfe38136c52e579800a432622baf5893b61365c7a18902a9ccd60
+```
+3. Generate the transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx --ethereum-sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 98.0 --open-capacity 1.0\
+ --tx-file tx.json
+```
+4. Sign the transaction
+```bash
+./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \
+ --tx-file tx.json
+```
+5. Add input, with capacity 99.99899588
+```bash
+./target/debug/examples/transfer_from_opentx add-input --tx-hash ebb9d9ff39efbee5957d6f7d19a4a17f1ac2e69dbc9289e4931cef6b832f4d57 --index 1 --tx-file tx.json
+```
+6. Add output, capacity is 99.99899588(original) + 1(open capacity) - 0.001(fee)
+```bash
+./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 100.99799588 --tx-file tx.json
+```
+7. Sighash sign the new input
+```bash
+./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json
+```
+8. Send the transaction
+```bash
+./target/debug/examples/transfer_from_opentx send --tx-file tx.json
+# 0x621077216f3bf7861beacd3cdda44f7a5854454fcd133922b89f0addd0330e6b
+```
+# Multisig open transaction example
+1. build an opentx address
+```bash
+./target/debug/examples/transfer_from_opentx build --require-first-n 0 \
+ --threshold 2 \
+ --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \
+ --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \
+ --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4
+```
+The output:
+```json
+{
+ "lock-arg": "0x065d7d0128eeaa6f9656a229b42aadd0b177d387eb10",
+ "lock-hash": "0xf5202949800af0b454b2e4806c57da1d0f3ae87f7b9f4b698d9f3b71162ec196",
+ "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqmtamce",
+ "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqqufhe3"
+}
+```
+2. Transfer capacity to the address
+```bash
+ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \
+ --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqqufhe3 \
+ --capacity 99 --skip-check-to-address
+# 0xf993b27a0129f72ec0a889cb016987c3cef00f7819461e51d5755464da6adf1b
+```
+3. Generate the transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx \
+ --require-first-n 0 \
+ --threshold 2 \
+ --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \
+ --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \
+ --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 98.0 --open-capacity 1.0 \
+ --tx-file tx.json
+```
+4. Sign the transaction, this step can sign seperately with each sender-key
+```bash
+./target/debug/examples/transfer_from_opentx sign-open-tx \
+ --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --sender-key d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \
+ --tx-file tx.json
+```
+5. Add input, with capacity 100.99799588
+```bash
+./target/debug/examples/transfer_from_opentx add-input --tx-hash 621077216f3bf7861beacd3cdda44f7a5854454fcd133922b89f0addd0330e6b --index 1 --tx-file tx.json
+```
+6. Add output, capacity is 100.99799588(original) + 1(open capacity) - 0.001(fee)
+```bash
+./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 101.99699588 --tx-file tx.json
+```
+7. Sighash sign the new input
+```bash
+./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json
+```
+8. Send the tx
+```bash
+./target/debug/examples/transfer_from_opentx send --tx-file tx.json
+# 0x577101b031d709992af99bd0715172bdb4d2eb7be9f11e84d6fb24ac3e1ac675
+```
+# Put multiple open transactions together
+1. Build/sign sighash open transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 97 --open-capacity 1\
+ --tx-file tx-sighash.json
+./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --tx-file tx-sighash.json
+```
+2. Build/sign sighash open transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx --ethereum-sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 97 --open-capacity 1\
+ --tx-file tx-ethereum.json
+./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \
+ --tx-file tx-ethereum.json
+```
+3. Build/sign multisig open transaction
+```bash
+./target/debug/examples/transfer_from_opentx gen-open-tx \
+ --require-first-n 0 \
+ --threshold 2 \
+ --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \
+ --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \
+ --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 \
+ --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \
+ --capacity 97 --open-capacity 1.0 \
+ --tx-file tx-multisig.json
+./target/debug/examples/transfer_from_opentx sign-open-tx \
+ --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \
+ --sender-key d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \
+ --tx-file tx-multisig.json
+```
+4. merge into one transaction
+
+You can merge them in one command:
+```bash
+./target/debug/examples/transfer_from_opentx merge-open-tx \
+ --in-tx-file tx-sighash.json \
+ --in-tx-file tx-ethereum.json \
+ --in-tx-file tx-multisig.json \
+ --tx-file tx.json
+```
+ The other way get the same merge result:
++ Merge first 2, then merge the last
+```bash
+./target/debug/examples/transfer_from_opentx merge-open-tx \
+ --in-tx-file tx-sighash.json \
+ --in-tx-file tx-ethereum.json \
+ --tx-file tx.json
+./target/debug/examples/transfer_from_opentx merge-open-tx \
+ --in-tx-file tx.json \
+ --in-tx-file tx-multisig.json \
+ --tx-file tx.json
+```
++ Merge last 2, then merge the first
+```bash
+./target/debug/examples/transfer_from_opentx merge-open-tx \
+ --in-tx-file tx-ethereum.json \
+ --in-tx-file tx-multisig.json \
+ --tx-file tx.json
+./target/debug/examples/transfer_from_opentx merge-open-tx \
+ --in-tx-file tx-sighash.json \
+ --in-tx-file tx.json \
+ --tx-file tx.json
+```
+5. Add input, with capacity 101.99699588
+```bash
+./target/debug/examples/transfer_from_opentx add-input --tx-hash 577101b031d709992af99bd0715172bdb4d2eb7be9f11e84d6fb24ac3e1ac675 --index 1 --tx-file tx.json
+```
+6. Add output, capacity is 101.99699588(original) + 3(1 open capacity each) - 0.001(fee)
+```bash
+./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 104.99599588 --tx-file tx.json
+```
+7. Sighash sign the new input
+```bash
+./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json
+```
+8. Send the transaction
+```bash
+./target/debug/examples/transfer_from_opentx send --tx-file tx.json
+# 0x4fd5d4adfb009a6e342a9e8442ac54989e28ef887b1fec60c3703e4c4d223b39
+```
\ No newline at end of file
diff --git a/examples/transfer_from_opentx.rs b/examples/transfer_from_opentx.rs
new file mode 100644
index 00000000..882dee5d
--- /dev/null
+++ b/examples/transfer_from_opentx.rs
@@ -0,0 +1,725 @@
+/*
+How to use the example transfer_from_opentx, see the file transfer_from_opentx.md
+*/
+use ckb_crypto::secp::Pubkey;
+use ckb_hash::blake2b_256;
+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, omni_lock::OmniLockTransferBuilder,
+ unlock_tx, CapacityBalancer, TxBuilder,
+ },
+ types::NetworkType,
+ unlock::{
+ opentx::{assembler::assemble_new_tx, OpentxWitness},
+ IdentityFlag, MultisigConfig, OmniLockConfig, OmniLockScriptSigner, SecpSighashUnlocker,
+ },
+ unlock::{OmniLockUnlocker, OmniUnlockMode, ScriptUnlocker},
+ util::{blake160, keccak160},
+ Address, HumanCapacity, ScriptGroup, ScriptId, SECP256K1,
+};
+use ckb_types::{
+ bytes::Bytes,
+ core::{BlockView, Capacity, ScriptHashType, TransactionView},
+ packed::{Byte32, CellDep, CellOutput, OutPoint, Script, Transaction, WitnessArgs},
+ prelude::*,
+ H160, H256,
+};
+use clap::{Args, Parser, Subcommand};
+use serde::{Deserialize, Serialize};
+use std::{collections::HashMap, error::Error as StdErr, fs, path::PathBuf};
+
+// The transaction hash i deployed my omnilock script with open transaction function on my test environment.
+// You should not use this hash to find your open transaction code_hash at any circumstances,
+// or you will lost your CKB for ever, or not make the example work.
+// You should replace this hash with your own transaction hash or provide one with command line parameter for this example.
+const OPENTX_TX_HASH: &str = "cfcc8aa04d963cb40d79eab17e6f8def536defca203b71a8fce9cfb950dd04fc";
+const OPENTX_TX_IDX: &str = "0";
+
+#[derive(Args)]
+struct MultiSigArgs {
+ /// Require first n signatures of corresponding pubkey
+ #[clap(long, value_name = "NUM", default_value = "1")]
+ require_first_n: u8,
+
+ /// Multisig threshold
+ #[clap(long, value_name = "NUM", default_value = "1")]
+ threshold: u8,
+
+ /// Normal sighash address
+ #[clap(long, value_name = "ADDRESS")]
+ sighash_address: Vec
,
+}
+#[derive(Args)]
+struct BuildOmniLockAddrArgs {
+ /// The receiver address
+ #[clap(long, value_name = "ADDRESS", group = "algorithm")]
+ receiver: Option,
+
+ /// The receiver's private key (hex string)
+ #[clap(long, value_name = "KEY", group = "algorithm")]
+ ethereum_receiver: Option,
+
+ #[clap(flatten)]
+ multis_args: MultiSigArgs,
+
+ /// omnilock script deploy transaction hash
+ #[clap(
+ long,
+ value_name = "H256",
+ default_value = OPENTX_TX_HASH
+ )]
+ omnilock_tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)]
+ 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 GenOpenTxArgs {
+ /// The sender private key (hex string)
+ #[clap(long, value_name = "KEY")]
+ sender_key: Option,
+ /// The sender private key (hex string)
+ #[clap(long, value_name = "KEY")]
+ ethereum_sender_key: Option,
+
+ #[clap(flatten)]
+ multis_args: MultiSigArgs,
+
+ /// The receiver address
+ #[clap(long, value_name = "ADDRESS")]
+ receiver: Address,
+
+ /// omnilock script deploy transaction hash
+ #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)]
+ omnilock_tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)]
+ omnilock_index: usize,
+
+ /// The capacity to transfer (unit: CKB, example: 102.43)
+ #[clap(long, value_name = "CKB")]
+ capacity: HumanCapacity,
+ /// The open transaction capacity not decided to whom (unit: CKB, example: 102.43)
+ #[clap(long, value_name = "CKB")]
+ open_capacity: HumanCapacity,
+ #[clap(long, value_name = "NUMBER", default_value = "0")]
+ fee_rate: u64,
+ /// 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", default_value = OPENTX_TX_HASH)]
+ omnilock_tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)]
+ 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 AddInputArgs {
+ /// omnilock script deploy transaction hash
+ #[clap(long, value_name = "H256")]
+ tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER")]
+ index: usize,
+
+ /// The output transaction info file (.json)
+ #[clap(long, value_name = "PATH")]
+ tx_file: PathBuf,
+
+ /// omnilock script deploy transaction hash
+ #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)]
+ omnilock_tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)]
+ 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 AddOutputArgs {
+ /// --to-sighash-address ckt1qyqg7zchpds6lv3v0nr36z2msu2x9a5lkhrq7kvyww --capacity 19999.9999 --tx-file tx.json
+ #[clap(long, value_name = "ADDRESS")]
+ to_address: Address,
+ /// 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,
+}
+
+#[derive(Args)]
+struct MergeOpenTxArgs {
+ /// The output transaction info file (.json)
+ #[clap(long, value_name = "PATH")]
+ in_tx_file: 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", default_value = OPENTX_TX_HASH)]
+ omnilock_tx_hash: H256,
+
+ /// cell index of omnilock script deploy transaction's outputs
+ #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)]
+ 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
+ GenOpenTx(GenOpenTxArgs),
+ /// Sign the open transaction
+ SignOpenTx(SignTxArgs),
+ /// sign sighash input
+ SighashSignTx(SignTxArgs),
+ /// merge opentx together
+ MergeOpenTx(MergeOpenTxArgs),
+ /// Add input
+ AddInput(AddInputArgs),
+ /// Add output
+ AddOutput(AddOutputArgs),
+ /// 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::GenOpenTx(gen_args) => {
+ gen_open_tx(&gen_args)?;
+ }
+ Commands::SignOpenTx(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();
+ if tx_info.omnilock_config.is_pubkey_hash() || tx_info.omnilock_config.is_ethereum() {
+ for (i, key) in keys.iter().enumerate() {
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, key);
+ let hash160 = match tx_info.omnilock_config.id().flag() {
+ IdentityFlag::PubkeyHash => {
+ blake2b_256(&pubkey.serialize()[..])[0..20].to_vec()
+ }
+ IdentityFlag::Ethereum => {
+ keccak160(Pubkey::from(pubkey).as_ref()).as_bytes().to_vec()
+ }
+ _ => unreachable!(),
+ };
+ if tx_info.omnilock_config.id().auth_content().as_bytes() != hash160 {
+ return Err(format!(
+ "key {:#x} is not in omnilock config",
+ args.sender_key[i]
+ )
+ .into());
+ }
+ }
+ }
+ 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 has been signed!");
+ } 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::SighashSignTx(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 (tx, _) = sighash_sign(&args, tx)?;
+ 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::AddInput(args) => {
+ let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?;
+ // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?);
+ let tx = Transaction::from(tx_info.tx.inner).into_view();
+ let tx = add_live_cell(&args, 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::AddOutput(args) => {
+ let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?;
+ // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?);
+ let tx = Transaction::from(tx_info.tx.inner).into_view();
+ let lock_script = Script::from(args.to_address.payload());
+ let output = CellOutput::new_builder()
+ .capacity(Capacity::shannons(args.capacity.0).pack())
+ .lock(lock_script)
+ .build();
+ let tx = tx
+ .as_advanced_builder()
+ .output(output)
+ .output_data(Bytes::default().pack())
+ .build();
+ 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! <<<");
+ }
+ Commands::MergeOpenTx(args) => {
+ let mut txes = vec![];
+ let mut omnilock_config = None;
+ for in_tx in &args.in_tx_file {
+ let tx_info: TxInfo = serde_json::from_slice(&fs::read(in_tx)?)?;
+ // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?);
+ let tx = Transaction::from(tx_info.tx.inner).into_view();
+ txes.push(tx);
+ omnilock_config = Some(tx_info.omnilock_config);
+ }
+ if !txes.is_empty() {
+ let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
+ let cell = build_omnilock_cell_dep(
+ &mut ckb_client,
+ &args.omnilock_tx_hash,
+ args.omnilock_index,
+ )?;
+ let tx_dep_provider =
+ DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10);
+ let tx = assemble_new_tx(txes, &tx_dep_provider, cell.type_hash.pack())?;
+ let tx_info = TxInfo {
+ tx: json_types::TransactionView::from(tx),
+ omnilock_config: omnilock_config.unwrap(),
+ };
+ fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?;
+ }
+ }
+ }
+
+ 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 mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
+ let cell =
+ build_omnilock_cell_dep(&mut ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?;
+ let mut config = if let Some(receiver) = args.receiver.as_ref() {
+ let arg = H160::from_slice(&receiver.payload().args()).unwrap();
+ OmniLockConfig::new_pubkey_hash(arg)
+ } else if let Some(ethereum_receiver) = args.ethereum_receiver.as_ref() {
+ let privkey = secp256k1::SecretKey::from_slice(ethereum_receiver.as_bytes()).unwrap();
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &privkey);
+ // println!("pubkey:{:?}", hex_string(&pubkey.serialize()));
+ // println!("pubkey:{:?}", hex_string(&pubkey.serialize_uncompressed()));
+ let addr = keccak160(Pubkey::from(pubkey).as_ref());
+ OmniLockConfig::new_ethereum(addr)
+ } else if !args.multis_args.sighash_address.is_empty() {
+ let args = &args.multis_args;
+ let multisig_config =
+ build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?;
+ OmniLockConfig::new_multisig(multisig_config)
+ } else {
+ return Err("must provide a receiver or an ethereum_receiver".into());
+ };
+ config.set_opentx_mode();
+ 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_open_tx(args: &GenOpenTxArgs) -> Result<(), Box> {
+ let (tx, omnilock_config) = build_open_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_open_tx(
+ args: &GenOpenTxArgs,
+) -> Result<(TransactionView, OmniLockConfig), Box> {
+ let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
+ let cell =
+ build_omnilock_cell_dep(&mut ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?;
+
+ let mut omnilock_config = if let Some(sender_key) = args.sender_key.as_ref() {
+ let sender_key = secp256k1::SecretKey::from_slice(sender_key.as_bytes())
+ .map_err(|err| format!("invalid sender secret key: {}", err))?;
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &sender_key);
+ let pubkey_hash = blake160(&pubkey.serialize());
+ OmniLockConfig::new_pubkey_hash(pubkey_hash)
+ } else if let Some(sender_key) = args.ethereum_sender_key.as_ref() {
+ let sender_key = secp256k1::SecretKey::from_slice(sender_key.as_bytes())
+ .map_err(|err| format!("invalid sender secret key: {}", err))?;
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &sender_key);
+ println!("pubkey:{:?}", hex_string(&pubkey.serialize()));
+ println!("pubkey:{:?}", hex_string(&pubkey.serialize_uncompressed()));
+ let addr = keccak160(Pubkey::from(pubkey).as_ref());
+ OmniLockConfig::new_ethereum(addr)
+ } else if !args.multis_args.sighash_address.is_empty() {
+ let args = &args.multis_args;
+ let multisig_config =
+ build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?;
+ OmniLockConfig::new_multisig(multisig_config)
+ } else {
+ return Err("must provide a sender-key or an ethereum-sender-key".into());
+ };
+ omnilock_config.set_opentx_mode();
+ // 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.clone(), placeholder_witness, args.fee_rate);
+
+ // Build:
+ // * CellDepResolver
+ // * HeaderDepResolver
+ // * CellCollector
+ // * TransactionDependencyProvider
+ let mut 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(sender.clone())
+ .capacity(args.capacity.0.pack())
+ .build();
+
+ let builder = OmniLockTransferBuilder::new_open(
+ args.open_capacity,
+ vec![(output, Bytes::default())],
+ omnilock_config.clone(),
+ None,
+ );
+
+ 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, _) = fill_placeholder_witnesses(base_tx, &tx_dep_provider, &unlockers)?;
+
+ let tx = balance_tx_capacity(
+ &tx,
+ &balancer,
+ &mut cell_collector,
+ &tx_dep_provider,
+ &cell_dep_resolver,
+ &header_dep_resolver,
+ )?;
+
+ let wit = OpentxWitness::new_sig_all_relative(&tx, Some(0xdeadbeef)).unwrap();
+ omnilock_config.set_opentx_input(wit);
+ let tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &omnilock_config,
+ OmniUnlockMode::Normal,
+ &tx_dep_provider,
+ &sender,
+ )?;
+ Ok((tx, omnilock_config))
+}
+
+fn build_omnilock_cell_dep(
+ ckb_client: &mut 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 add_live_cell(
+ args: &AddInputArgs,
+ tx: TransactionView,
+) -> Result> {
+ let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
+ let out_point_json = ckb_jsonrpc_types::OutPoint {
+ tx_hash: args.tx_hash.clone(),
+ index: ckb_jsonrpc_types::Uint32::from(args.index as u32),
+ };
+ let cell_with_status = ckb_client.get_live_cell(out_point_json, false)?;
+ let input_outpoint = OutPoint::new(
+ Byte32::from_slice(args.tx_hash.as_bytes())?,
+ args.index as u32,
+ );
+ // since value should be provided in args
+ let input = ckb_types::packed::CellInput::new(input_outpoint, 0);
+ let cell_dep_resolver = {
+ let genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap();
+ DefaultCellDepResolver::from_genesis(&BlockView::from(genesis_block))?
+ };
+ let code_hash = cell_with_status.cell.unwrap().output.lock.code_hash;
+ let script_id = ScriptId::new_type(code_hash);
+ let dep = cell_dep_resolver
+ .get(&script_id)
+ .as_ref()
+ .unwrap()
+ .0
+ .clone();
+
+ Ok(tx.as_advanced_builder().input(input).cell_dep(dep).build())
+}
+
+fn build_omnilock_unlockers(
+ keys: Vec,
+ config: OmniLockConfig,
+ omni_lock_type_hash: H256,
+) -> HashMap> {
+ let signer = match config.id().flag() {
+ IdentityFlag::PubkeyHash => SecpCkbRawKeySigner::new_with_secret_keys(keys),
+ IdentityFlag::Ethereum => SecpCkbRawKeySigner::new_with_ethereum_secret_keys(keys),
+ IdentityFlag::Multisig => SecpCkbRawKeySigner::new_with_secret_keys(keys),
+ _ => unreachable!("should not reach here!"),
+ };
+ 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 mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str());
+ let cell =
+ build_omnilock_cell_dep(&mut 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()))
+}
+
+fn sighash_sign(
+ args: &SignTxArgs,
+ tx: TransactionView,
+) -> Result<(TransactionView, Vec), Box> {
+ if args.sender_key.is_empty() {
+ return Err("must provide sender-key to sign".into());
+ }
+ let sender_key = secp256k1::SecretKey::from_slice(args.sender_key[0].as_bytes())
+ .map_err(|err| format!("invalid sender secret key: {}", err))?;
+ // Build ScriptUnlocker
+ let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![sender_key]);
+ let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>);
+ let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone());
+ let mut unlockers = HashMap::default();
+ unlockers.insert(
+ sighash_script_id,
+ Box::new(sighash_unlocker) as Box,
+ );
+
+ // Build the transaction
+ // 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, still_locked_groups) = builder.build_unlocked(
+ // &mut cell_collector,
+ // &cell_dep_resolver,
+ // &header_dep_resolver,
+ // &tx_dep_provider,
+ // &balancer,
+ // &unlockers,
+ // )?;
+
+ let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10);
+ let (new_tx, new_still_locked_groups) = unlock_tx(tx, &tx_dep_provider, &unlockers)?;
+ Ok((new_tx, new_still_locked_groups))
+}
diff --git a/src/test-data/omni_lock b/src/test-data/omni_lock
index 96c90c33..9689bbaf 100755
Binary files a/src/test-data/omni_lock and b/src/test-data/omni_lock differ
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 73a46a43..bce3ff3e 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -1029,11 +1029,7 @@ fn test_udt_transfer() {
ctx.add_live_cell(receiver_input, receiver_output.clone(), receiver_data, None);
let udt_receiver = UdtTargetReceiver::new(TransferAction::Update, receiver_acp_lock, 300);
- let builder = UdtTransferBuilder {
- type_script,
- sender: sender.clone(),
- receivers: vec![udt_receiver],
- };
+ let builder = UdtTransferBuilder::new(type_script, sender.clone(), vec![udt_receiver]);
let placeholder_witness = WitnessArgs::new_builder()
.lock(Some(Bytes::from(vec![0u8; 65])).pack())
.build();
@@ -1087,3 +1083,4 @@ fn test_udt_transfer() {
pub mod omni_lock;
pub mod omni_lock_util;
+pub mod opentx;
diff --git a/src/tests/omni_lock.rs b/src/tests/omni_lock.rs
index cdcd3c73..1fce0292 100644
--- a/src/tests/omni_lock.rs
+++ b/src/tests/omni_lock.rs
@@ -40,9 +40,9 @@ use rand::Rng;
use crate::tx_builder::{unlock_tx, CapacityBalancer, TxBuilder};
-const OMNILOCK_BIN: &[u8] = include_bytes!("../test-data/omni_lock");
+pub const OMNILOCK_BIN: &[u8] = include_bytes!("../test-data/omni_lock");
-fn build_omnilock_script(cfg: &OmniLockConfig) -> Script {
+pub fn build_omnilock_script(cfg: &OmniLockConfig) -> Script {
let omnilock_data_hash = H256::from(blake2b_256(OMNILOCK_BIN));
Script::new_builder()
.code_hash(omnilock_data_hash.pack())
@@ -51,7 +51,7 @@ fn build_omnilock_script(cfg: &OmniLockConfig) -> Script {
.build()
}
-fn build_omnilock_unlockers(
+pub fn build_omnilock_unlockers(
key: secp256k1::SecretKey,
config: OmniLockConfig,
unlock_mode: OmniUnlockMode,
@@ -117,13 +117,12 @@ fn test_omnilock_simple_hash(cfg: OmniLockConfig) {
CapacityBalancer::new_simple(sender.clone(), placeholder_witness.clone(), FEE_RATE);
let mut cell_collector = ctx.to_live_cells_context();
- let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
- let unlockers = build_omnilock_unlockers(account2_key, cfg.clone(), unlock_mode);
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account0_key, cfg, unlock_mode);
let mut tx = builder
.build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
.unwrap();
- let unlockers = build_omnilock_unlockers(account2_key, cfg, unlock_mode);
let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
assert!(new_locked_groups.is_empty());
tx = new_tx;
@@ -1172,11 +1171,7 @@ fn test_omnilock_udt_transfer() {
ctx.add_live_cell(receiver_input, receiver_output.clone(), receiver_data, None);
let udt_receiver = UdtTargetReceiver::new(TransferAction::Update, receiver_acp_lock, 300);
- let builder = UdtTransferBuilder {
- type_script,
- sender: sender.clone(),
- receivers: vec![udt_receiver],
- };
+ let builder = UdtTransferBuilder::new(type_script, sender.clone(), vec![udt_receiver]);
let placeholder_witness = WitnessArgs::new_builder()
.lock(Some(Bytes::from(vec![0u8; 65])).pack())
.build();
diff --git a/src/tests/opentx.rs b/src/tests/opentx.rs
new file mode 100644
index 00000000..bdbdd646
--- /dev/null
+++ b/src/tests/opentx.rs
@@ -0,0 +1,751 @@
+use ckb_hash::blake2b_256;
+use ckb_jsonrpc_types as json_types;
+use std::collections::HashMap;
+
+use crate::{
+ constants::{ONE_CKB, SIGHASH_TYPE_HASH},
+ test_util::random_out_point,
+ tests::{
+ build_sighash_script, init_context,
+ omni_lock::{build_omnilock_script, build_omnilock_unlockers, OMNILOCK_BIN},
+ ACCOUNT0_ARG, ACCOUNT0_KEY, ACCOUNT1_ARG, ACCOUNT1_KEY, ACCOUNT2_ARG, ACCOUNT2_KEY,
+ ACCOUNT3_ARG, ACCOUNT3_KEY, FEE_RATE, SUDT_BIN,
+ },
+ traits::{CellCollector, CellQueryOptions, SecpCkbRawKeySigner},
+ tx_builder::{
+ omni_lock::OmniLockTransferBuilder, transfer::CapacityTransferBuilder,
+ udt::UdtTransferBuilder,
+ },
+ unlock::{
+ opentx::OpentxWitness, MultisigConfig, OmniLockConfig, OmniUnlockMode, ScriptUnlocker,
+ SecpSighashUnlocker,
+ },
+ util::{blake160, keccak160},
+ ScriptId,
+};
+
+use ckb_crypto::secp::{Pubkey, SECP256K1};
+use ckb_types::{
+ bytes::Bytes,
+ core::{Capacity, ScriptHashType},
+ packed::{CellInput, CellOutput, Script, WitnessArgs},
+ prelude::*,
+ H160, H256,
+};
+use rand::Rng;
+
+use crate::tx_builder::{unlock_tx, CapacityBalancer, TxBuilder};
+const ZERO_FEE_RATE: u64 = 0;
+
+fn build_simple_config(key: H256) -> OmniLockConfig {
+ let priv_key = secp256k1::SecretKey::from_slice(key.as_bytes())
+ .map_err(|err| format!("invalid sender secret key: {}", err))
+ .unwrap();
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &priv_key);
+ OmniLockConfig::new_pubkey_hash(blake160(&pubkey.serialize()))
+}
+#[test]
+fn test_opentx_pay_from_sighash() {
+ let cfg = build_simple_config(ACCOUNT0_KEY);
+ test_opentx_pay_simple_hash(cfg);
+}
+
+#[test]
+fn test_opentx_pay_from_ethereum() {
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &account0_key);
+ let cfg = OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref()));
+ test_opentx_pay_simple_hash(cfg);
+}
+
+/// account0(200) => account0(exchange 199) + open pay 1,
+/// account2(100) => account2(101 - transaction fee)
+fn test_opentx_pay_simple_hash(mut cfg: OmniLockConfig) {
+ cfg.set_opentx_mode();
+ let unlock_mode = OmniUnlockMode::Normal;
+ let sender = build_omnilock_script(&cfg);
+ let receiver = build_sighash_script(ACCOUNT2_ARG);
+
+ let ctx = init_context(
+ vec![(OMNILOCK_BIN, true)],
+ vec![
+ (sender.clone(), Some(200 * ONE_CKB)),
+ (receiver.clone(), Some(100 * ONE_CKB)),
+ (receiver.clone(), Some(200 * ONE_CKB)),
+ ],
+ );
+
+ let output = CellOutput::new_builder()
+ .capacity((199 * ONE_CKB).pack())
+ .lock(sender.clone())
+ .build();
+ let builder = OmniLockTransferBuilder::new_open(
+ ONE_CKB.into(),
+ vec![(output.clone(), Bytes::default())],
+ cfg.clone(),
+ None,
+ );
+ let placeholder_witness = cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE);
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ let mut rng = rand::thread_rng();
+ let salt: u32 = rng.gen();
+ let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap();
+ cfg.set_opentx_input(wit);
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &sender,
+ )
+ .unwrap();
+ // config updated, so unlockers must rebuilt.
+ let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert!(new_locked_groups.is_empty());
+ tx = new_tx;
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ // use the opentx
+
+ // Build ScriptUnlocker
+ let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap();
+ let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![account2_key]);
+ let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>);
+ let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone());
+ let mut unlockers = HashMap::default();
+ unlockers.insert(
+ sighash_script_id,
+ Box::new(sighash_unlocker) as Box,
+ );
+
+ // Build CapacityBalancer
+ let placeholder_witness = WitnessArgs::new_builder()
+ .lock(Some(Bytes::from(vec![0u8; 65])).pack())
+ .build();
+ let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, 1000);
+ // // Build the transaction
+ let query = CellQueryOptions::new_lock(receiver.clone());
+ let (inputs, total_capacity) = cell_collector.collect_live_cells(&query, false).unwrap();
+ let input = &inputs[0];
+ let input_output = &input.out_point;
+ println!("{:#x} total_capacity: {}", input_output, total_capacity);
+ // let output = CellOutput::new_builder()
+ // .lock(receiver.clone())
+ // .capacity((100 * ONE_CKB).pack())
+ // .build();
+ let builder = CapacityTransferBuilder::new_with_transaction(
+ vec![/*(output.clone(), Bytes::default())*/],
+ tx,
+ );
+ let (tx, still_locked_groups) = builder
+ .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ assert_eq!(1, still_locked_groups.len());
+
+ assert_eq!(tx.header_deps().len(), 0);
+ assert_eq!(tx.cell_deps().len(), 2);
+ assert_eq!(tx.inputs().len(), 2);
+ assert_eq!(tx.outputs().len(), 2);
+ assert_eq!(tx.output(0).unwrap(), output);
+ let output1 = tx.output(1).unwrap();
+ assert_eq!(output1.lock(), receiver);
+ let receiver_capacity: u64 = output1.capacity().unpack();
+ assert!(receiver_capacity - 100 * ONE_CKB < ONE_CKB);
+ assert_eq!(tx.witnesses().len(), 2);
+ ctx.verify(tx, FEE_RATE).unwrap();
+}
+
+/// multisig(200) => multisig(exchange 199) + open pay 1, locked by account0, account1, account2
+/// account3(400) => account2(401 - transaction fee)
+#[test]
+fn test_opentx_pay_from_multisig() {
+ let unlock_mode = OmniUnlockMode::Normal;
+ let lock_args = vec![
+ ACCOUNT0_ARG.clone(),
+ ACCOUNT1_ARG.clone(),
+ ACCOUNT2_ARG.clone(),
+ ];
+ let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap();
+ let mut cfg = OmniLockConfig::new_multisig(multi_cfg);
+ cfg.set_opentx_mode();
+
+ let sender = build_omnilock_script(&cfg);
+ let receiver = build_sighash_script(ACCOUNT3_ARG);
+
+ let ctx = init_context(
+ vec![(OMNILOCK_BIN, true)],
+ vec![
+ (sender.clone(), Some(200 * ONE_CKB)),
+ (sender.clone(), Some(300 * ONE_CKB)),
+ (receiver.clone(), Some(400 * ONE_CKB)),
+ (receiver.clone(), Some(500 * ONE_CKB)),
+ (receiver.clone(), Some(600 * ONE_CKB)),
+ ],
+ );
+
+ let output = CellOutput::new_builder()
+ .capacity((199 * ONE_CKB).pack())
+ .lock(sender.clone())
+ .build();
+ let builder = OmniLockTransferBuilder::new_open(
+ ONE_CKB.into(),
+ vec![(output.clone(), Bytes::default())],
+ cfg.clone(),
+ None,
+ );
+ let placeholder_witness = cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE);
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ // add opentx hash data
+ let mut rng = rand::thread_rng();
+ let salt: u32 = rng.gen();
+ let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap();
+ cfg.set_opentx_input(wit);
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &sender,
+ )
+ .unwrap();
+ for key in [account0_key, account2_key] {
+ let unlockers = build_omnilock_unlockers(key, cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert!(new_locked_groups.is_empty());
+ tx = new_tx;
+ }
+
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ // use the opentx
+
+ // Build ScriptUnlocker
+ let account3_key = secp256k1::SecretKey::from_slice(ACCOUNT3_KEY.as_bytes()).unwrap();
+ let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![account3_key]);
+ let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>);
+ let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone());
+ let mut unlockers = HashMap::default();
+ unlockers.insert(
+ sighash_script_id,
+ Box::new(sighash_unlocker) as Box,
+ );
+
+ // Build CapacityBalancer
+ let placeholder_witness = WitnessArgs::new_builder()
+ .lock(Some(Bytes::from(vec![0u8; 65])).pack())
+ .build();
+ let balancer =
+ CapacityBalancer::new_simple(receiver.clone(), placeholder_witness.clone(), 1000);
+ // // Build the transaction
+ let query = CellQueryOptions::new_lock(receiver.clone());
+ let (inputs, total_capacity) = cell_collector.collect_live_cells(&query, false).unwrap();
+ let input = &inputs[0];
+ let input_output = &input.out_point;
+ println!("{:#x} total_capacity: {}", input_output, total_capacity);
+ // let output = CellOutput::new_builder()
+ // .lock(receiver.clone())
+ // .capacity((100 * ONE_CKB).pack())
+ // .build();
+ let builder = CapacityTransferBuilder::new_with_transaction(
+ vec![/*(output.clone(), Bytes::default())*/],
+ tx,
+ );
+ let (tx, still_locked_groups) = builder
+ .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ assert_eq!(1, still_locked_groups.len());
+ assert_eq!(tx.header_deps().len(), 0);
+ assert_eq!(tx.cell_deps().len(), 2);
+ assert_eq!(tx.inputs().len(), 2);
+ assert_eq!(tx.outputs().len(), 2);
+ assert_eq!(tx.output(0).unwrap(), output);
+ let output1 = tx.output(1).unwrap();
+ assert_eq!(output1.lock(), receiver);
+ let receiver_capacity: u64 = output1.capacity().unpack();
+ assert!(receiver_capacity - 400 * ONE_CKB < ONE_CKB);
+ let witnesses = tx
+ .witnesses()
+ .into_iter()
+ .map(|w| w.raw_data())
+ .collect::>();
+ assert_eq!(witnesses.len(), 2);
+ assert_eq!(witnesses[1].len(), placeholder_witness.as_slice().len());
+ ctx.verify(tx, FEE_RATE).unwrap();
+}
+
+#[test]
+fn test_opentx_pay_receive_sighash_absolute_from_start() {
+ test_opentx_pay_receive_sighash_absolute(true);
+}
+#[test]
+fn test_opentx_pay_receive_sighash_absolute_self() {
+ test_opentx_pay_receive_sighash_absolute(false);
+}
+fn test_opentx_pay_receive_sighash_absolute(from_start: bool) {
+ let sender_cfg = build_simple_config(ACCOUNT0_KEY);
+ let receiver_cfg = build_simple_config(ACCOUNT2_KEY);
+ test_opentx_pay_receive_simple_hash_absolute(sender_cfg, receiver_cfg, from_start);
+}
+
+#[test]
+fn test_opentx_pay_receive_ethereum_absolute_from_start() {
+ test_opentx_pay_receive_ethereum_absolute(true);
+}
+#[test]
+fn test_opentx_pay_receive_ethereum_absolute_from_self() {
+ test_opentx_pay_receive_ethereum_absolute(false);
+}
+fn test_opentx_pay_receive_ethereum_absolute(from_start: bool) {
+ let cfgs: Vec = [ACCOUNT0_KEY, ACCOUNT2_KEY]
+ .iter()
+ .map(|key| {
+ let priv_key = secp256k1::SecretKey::from_slice(key.as_bytes()).unwrap();
+ let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &priv_key);
+ OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref()))
+ })
+ .collect();
+ test_opentx_pay_receive_simple_hash_absolute(cfgs[0].clone(), cfgs[1].clone(), from_start);
+}
+
+/// account0(200) => account0(exchange 199) + open pay 1,
+/// account2(100) => account2(101 - transaction fee)
+fn test_opentx_pay_receive_simple_hash_absolute(
+ mut sender_cfg: OmniLockConfig,
+ mut receiver_cfg: OmniLockConfig,
+ from_start: bool,
+) {
+ sender_cfg.set_opentx_mode();
+ receiver_cfg.set_opentx_mode();
+ let unlock_mode = OmniUnlockMode::Normal;
+ let sender = build_omnilock_script(&sender_cfg);
+ let receiver = build_omnilock_script(&receiver_cfg);
+
+ let ctx = init_context(
+ vec![(OMNILOCK_BIN, true)],
+ vec![
+ (sender.clone(), Some(200 * ONE_CKB)),
+ (receiver.clone(), Some(100 * ONE_CKB)),
+ (receiver.clone(), Some(200 * ONE_CKB)),
+ ],
+ );
+
+ let output = CellOutput::new_builder()
+ .capacity((199 * ONE_CKB).pack())
+ .lock(sender.clone())
+ .build();
+ let builder = OmniLockTransferBuilder::new_open(
+ (ONE_CKB).into(),
+ vec![(output.clone(), Bytes::default())],
+ sender_cfg.clone(),
+ None,
+ );
+ let placeholder_witness = sender_cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE);
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ let mut rng = rand::thread_rng();
+ let salt: u32 = rng.gen();
+ let wit = OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap();
+ sender_cfg.set_opentx_input(wit);
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &sender_cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &sender,
+ )
+ .unwrap();
+ // config updated, so unlockers must rebuilt.
+ let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert!(new_locked_groups.is_empty());
+ tx = new_tx;
+
+ // use the opentx
+ let opentx_input_len = tx.inputs().len();
+ let opentx_output_len = tx.outputs().len();
+ receiver_cfg.set_opentx_reserve_bytes_by_commands(20);
+ // Build ScriptUnlocker
+ let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account2_key, receiver_cfg.clone(), unlock_mode);
+
+ // Build CapacityBalancer
+ let placeholder_witness = receiver_cfg.placeholder_witness(unlock_mode).unwrap();
+ // why + 100? After update openwitness input list, will need tens of bytes more, if not +100, after update, should calculate adjust the fee again.
+ // If adjust the transaction fee later, the exchange may mot be enough to maintain the minimal capacity.
+ let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, FEE_RATE);
+
+ let builder = CapacityTransferBuilder::new_with_transaction(
+ vec![/*(output.clone(), Bytes::default())*/],
+ tx,
+ );
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ assert_eq!(opentx_input_len + 1, tx.inputs().len());
+ assert_eq!(opentx_output_len + 1, tx.outputs().len());
+
+ let salt: u32 = rng.gen();
+ let mut wit = if from_start {
+ OpentxWitness::new_sig_all_absolute(&tx, Some(salt))
+ } else {
+ OpentxWitness::new_sig_to_end_absolute(&tx, Some(salt), opentx_input_len, opentx_output_len)
+ }
+ .unwrap(); //OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap();
+ wit.add_tx_hash_input();
+ receiver_cfg.set_opentx_input(wit);
+
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &receiver_cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &receiver,
+ )
+ .unwrap();
+
+ // config updated, so unlockers must rebuilt.
+ let unlockers = build_omnilock_unlockers(account2_key, receiver_cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+
+ assert_eq!(1, new_locked_groups.len());
+ tx = new_tx;
+
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+
+ assert_eq!(tx.header_deps().len(), 0);
+ assert_eq!(tx.cell_deps().len(), 1);
+ assert_eq!(tx.inputs().len(), 2);
+ assert_eq!(tx.outputs().len(), 2);
+ assert_eq!(tx.output(0).unwrap(), output);
+ let output1 = tx.output(1).unwrap();
+ assert_eq!(output1.lock(), receiver);
+ let receiver_capacity: u64 = output1.capacity().unpack();
+ assert!(receiver_capacity - 100 * ONE_CKB < ONE_CKB);
+ assert_eq!(tx.witnesses().len(), 2);
+ ctx.verify(tx, FEE_RATE).unwrap();
+}
+#[test]
+fn test_opentx_pay_receive_multisig_absolute_from_start() {
+ test_opentx_pay_receive_multisig_absolute(true);
+}
+
+#[test]
+fn test_opentx_pay_receive_multisig_absolute_from_self() {
+ test_opentx_pay_receive_multisig_absolute(false);
+}
+
+/// multisig(200) => multisig(exchange 199) + open pay 1, locked by account0, account1, account2
+/// account3(400) => account2(401 - transaction fee)
+fn test_opentx_pay_receive_multisig_absolute(from_start: bool) {
+ let unlock_mode = OmniUnlockMode::Normal;
+ let lock_args = vec![
+ ACCOUNT0_ARG.clone(),
+ ACCOUNT1_ARG.clone(),
+ ACCOUNT2_ARG.clone(),
+ ];
+ let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap();
+ let mut sender_cfg = OmniLockConfig::new_multisig(multi_cfg);
+ sender_cfg.set_opentx_mode();
+
+ let sender = build_omnilock_script(&sender_cfg);
+ let lock_args = vec![
+ ACCOUNT1_ARG.clone(),
+ ACCOUNT2_ARG.clone(),
+ ACCOUNT3_ARG.clone(),
+ ];
+ let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap();
+ let mut receiver_cfg = OmniLockConfig::new_multisig(multi_cfg);
+ receiver_cfg.set_opentx_mode();
+ let receiver = build_omnilock_script(&receiver_cfg);
+
+ let ctx = init_context(
+ vec![(OMNILOCK_BIN, true)],
+ vec![
+ (sender.clone(), Some(200 * ONE_CKB)),
+ (sender.clone(), Some(300 * ONE_CKB)),
+ (receiver.clone(), Some(400 * ONE_CKB)),
+ (receiver.clone(), Some(500 * ONE_CKB)),
+ (receiver.clone(), Some(600 * ONE_CKB)),
+ ],
+ );
+
+ let output = CellOutput::new_builder()
+ .capacity((199 * ONE_CKB).pack())
+ .lock(sender.clone())
+ .build();
+ let builder = OmniLockTransferBuilder::new_open(
+ ONE_CKB.into(),
+ vec![(output.clone(), Bytes::default())],
+ sender_cfg.clone(),
+ None,
+ );
+ let placeholder_witness = sender_cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE);
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap();
+ let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ // add opentx hash data
+ let mut rng = rand::thread_rng();
+ let salt: u32 = rng.gen();
+ let wit = OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap();
+ sender_cfg.set_opentx_input(wit);
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &sender_cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &sender,
+ )
+ .unwrap();
+ for key in [account0_key, account2_key] {
+ let unlockers = build_omnilock_unlockers(key, sender_cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert!(new_locked_groups.is_empty());
+ tx = new_tx;
+ }
+
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ // use the opentx
+ let opentx_input_len = tx.inputs().len();
+ let opentx_output_len = tx.outputs().len();
+ receiver_cfg.set_opentx_reserve_bytes_by_commands(20);
+ // Build ScriptUnlocker
+ let account1_key = secp256k1::SecretKey::from_slice(ACCOUNT1_KEY.as_bytes()).unwrap();
+ let account3_key = secp256k1::SecretKey::from_slice(ACCOUNT3_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(account1_key, receiver_cfg.clone(), unlock_mode);
+ // Build CapacityBalancer
+ let placeholder_witness = receiver_cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, FEE_RATE);
+
+ let builder = CapacityTransferBuilder::new_with_transaction(vec![], tx);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ assert_eq!(opentx_input_len + 1, tx.inputs().len());
+ assert_eq!(opentx_output_len + 1, tx.outputs().len());
+
+ let salt: u32 = rng.gen();
+ let mut wit = if from_start {
+ OpentxWitness::new_sig_all_absolute(&tx, Some(salt))
+ } else {
+ OpentxWitness::new_sig_to_end_absolute(&tx, Some(salt), opentx_input_len, opentx_output_len)
+ }
+ .unwrap(); //OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap();
+ wit.add_tx_hash_input();
+ receiver_cfg.set_opentx_input(wit);
+
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &receiver_cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &receiver,
+ )
+ .unwrap();
+
+ for key in [account1_key, account3_key] {
+ let unlockers = build_omnilock_unlockers(key, receiver_cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert_eq!(1, new_locked_groups.len());
+ tx = new_tx;
+ }
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ assert_eq!(tx.header_deps().len(), 0);
+ assert_eq!(tx.cell_deps().len(), 1);
+ assert_eq!(tx.inputs().len(), 2);
+ assert_eq!(tx.outputs().len(), 2);
+ assert_eq!(tx.output(0).unwrap(), output);
+ let output1 = tx.output(1).unwrap();
+ assert_eq!(output1.lock(), receiver);
+ let receiver_capacity: u64 = output1.capacity().unpack();
+ assert!(receiver_capacity - 400 * ONE_CKB < ONE_CKB);
+
+ assert_eq!(tx.witnesses().len(), 2);
+ ctx.verify(tx, FEE_RATE).unwrap();
+}
+
+#[test]
+fn test_opentx_udt_open_buy() {
+ // ACCOUNT1(alice) will spend 50.01 CKB with fee to buy 1,000,000 SUDT
+ // ACCOUNT2(bob) collect the 50 CKB with the transfer 1,000,000 SUDT
+ let unlock_mode = OmniUnlockMode::Normal;
+ let mut alice_cfg = build_simple_config(ACCOUNT1_KEY);
+ alice_cfg.set_opentx_mode();
+ let alice = build_omnilock_script(&alice_cfg);
+ let bob = build_sighash_script(ACCOUNT2_ARG);
+
+ let mut ctx = init_context(
+ vec![(OMNILOCK_BIN, true), (SUDT_BIN, false)],
+ vec![
+ (alice.clone(), Some(300 * ONE_CKB)),
+ (bob.clone(), Some(400 * ONE_CKB)),
+ ],
+ );
+ let sudt_data_hash = H256::from(blake2b_256(SUDT_BIN));
+ let owner = build_sighash_script(H160::default());
+ let type_script = Script::new_builder()
+ .code_hash(sudt_data_hash.pack())
+ .hash_type(ScriptHashType::Data1.into())
+ .args(owner.calc_script_hash().as_bytes().pack())
+ .build();
+ let sudt_input = CellInput::new(random_out_point(), 0);
+ let sudt_output = CellOutput::new_builder()
+ .capacity(ONE_CKB.pack())
+ .lock(bob.clone())
+ .type_(Some(type_script.clone()).pack())
+ .build();
+ let sudt_capacity = sudt_output
+ .occupied_capacity(Capacity::bytes(16).unwrap())
+ .unwrap()
+ .as_u64();
+ println!("sudt_capacity: {}", sudt_capacity);
+ let sudt_output = sudt_output
+ .as_builder()
+ .capacity(sudt_capacity.pack())
+ .build();
+ let sudt_data = Bytes::from(1_000_000u128.to_le_bytes().to_vec());
+ ctx.add_live_cell(sudt_input, sudt_output, sudt_data.clone(), None);
+
+ let fee = 100_0000u64;
+ // build opentx alice's input
+ let builder = OmniLockTransferBuilder::new_open(
+ (50 * ONE_CKB + sudt_capacity + fee).into(),
+ vec![],
+ alice_cfg.clone(),
+ None,
+ );
+ let placeholder_witness = alice_cfg.placeholder_witness(unlock_mode).unwrap();
+ let balancer = CapacityBalancer::new_simple(alice.clone(), placeholder_witness, ZERO_FEE_RATE);
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let alice_key = secp256k1::SecretKey::from_slice(ACCOUNT1_KEY.as_bytes()).unwrap();
+ let unlockers = build_omnilock_unlockers(alice_key, alice_cfg.clone(), unlock_mode);
+ let mut tx = builder
+ .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ // add sudt output
+ let sudt_output = CellOutput::new_builder()
+ .capacity((sudt_capacity).pack())
+ .lock(alice.clone())
+ .type_(Some(type_script.clone()).pack())
+ .build();
+ tx = tx
+ .as_advanced_builder()
+ .output(sudt_output.clone())
+ .output_data(sudt_data.pack())
+ .build();
+ // update opentx input list
+ let mut rng = rand::thread_rng();
+ let salt: u32 = rng.gen();
+ let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap();
+ alice_cfg.set_opentx_input(wit);
+ tx = OmniLockTransferBuilder::update_opentx_witness(
+ tx,
+ &alice_cfg,
+ OmniUnlockMode::Normal,
+ &ctx,
+ &alice,
+ )
+ .unwrap();
+ // config updated, so unlockers must rebuilt.
+ let unlockers = build_omnilock_unlockers(alice_key, alice_cfg.clone(), unlock_mode);
+ let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap();
+ assert!(new_locked_groups.is_empty());
+ tx = new_tx;
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ // use opentx
+ let builder = UdtTransferBuilder::new_with_transaction(type_script, bob.clone(), vec![], tx);
+ let placeholder_witness = WitnessArgs::new_builder()
+ .lock(Some(Bytes::from(vec![0u8; 65])).pack())
+ .build();
+ let balancer = CapacityBalancer::new_simple(bob, placeholder_witness, FEE_RATE);
+
+ let bob_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap();
+ let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![bob_key]);
+ let script_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>);
+ let mut unlockers: HashMap> = HashMap::default();
+ unlockers.insert(
+ ScriptId::new_type(SIGHASH_TYPE_HASH.clone()),
+ Box::new(script_unlocker),
+ );
+
+ let mut cell_collector = ctx.to_live_cells_context();
+ let (tx, locked_groups) = builder
+ .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers)
+ .unwrap();
+ println!(
+ "> tx: {}",
+ serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap()
+ );
+ assert_eq!(locked_groups.len(), 1);
+ assert_eq!(tx.header_deps().len(), 0);
+ assert_eq!(tx.cell_deps().len(), 3);
+ assert_eq!(tx.inputs().len(), 3);
+ let outputs = tx.outputs().into_iter().collect::>();
+ assert_eq!(outputs.len(), 4);
+ assert_eq!(outputs[1], sudt_output);
+ let expected_outputs_data = vec![
+ Bytes::from(1_000_000u128.to_le_bytes().to_vec()),
+ Bytes::from(0u128.to_le_bytes().to_vec()),
+ ];
+ let outputs_data = tx
+ .outputs_data()
+ .into_iter()
+ .map(|d| d.raw_data())
+ .collect::>();
+ assert_eq!(outputs_data[1..3], expected_outputs_data);
+ ctx.verify(tx, FEE_RATE).unwrap();
+}
diff --git a/src/tx_builder/mod.rs b/src/tx_builder/mod.rs
index ae26f0f8..4981f52e 100644
--- a/src/tx_builder/mod.rs
+++ b/src/tx_builder/mod.rs
@@ -164,6 +164,14 @@ pub fn tx_fee(
tx: TransactionView,
tx_dep_provider: &dyn TransactionDependencyProvider,
header_dep_resolver: &dyn HeaderDepResolver,
+) -> Result {
+ tx_fee_with_open(tx, tx_dep_provider, header_dep_resolver, 0)
+}
+pub fn tx_fee_with_open(
+ tx: TransactionView,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
+ header_dep_resolver: &dyn HeaderDepResolver,
+ open_capacity: u64,
) -> Result {
let mut input_total: u64 = 0;
for input in tx.inputs() {
@@ -218,7 +226,7 @@ pub fn tx_fee(
};
input_total += capacity;
}
- let output_total = tx.outputs_capacity()?.as_u64();
+ let output_total = tx.outputs_capacity()?.as_u64() + open_capacity;
#[allow(clippy::unnecessary_lazy_evaluations)]
input_total
.checked_sub(output_total)
@@ -359,6 +367,26 @@ pub fn balance_tx_capacity(
tx_dep_provider: &dyn TransactionDependencyProvider,
cell_dep_resolver: &dyn CellDepResolver,
header_dep_resolver: &dyn HeaderDepResolver,
+) -> Result {
+ balance_tx_capacity_with_open(
+ tx,
+ balancer,
+ cell_collector,
+ tx_dep_provider,
+ cell_dep_resolver,
+ header_dep_resolver,
+ 0,
+ )
+}
+/// Fill more inputs to balance the transaction capacity
+pub fn balance_tx_capacity_with_open(
+ tx: &TransactionView,
+ balancer: &CapacityBalancer,
+ cell_collector: &mut dyn CellCollector,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
+ cell_dep_resolver: &dyn CellDepResolver,
+ header_dep_resolver: &dyn HeaderDepResolver,
+ open_capacity: u64,
) -> Result {
let capacity_provider = &balancer.capacity_provider;
if capacity_provider.lock_scripts.is_empty() {
@@ -403,6 +431,7 @@ pub fn balance_tx_capacity(
let cell = tx_dep_provider.get_cell(&input.previous_output())?;
if cell.lock() == *lock_script {
has_provider = true;
+ break;
}
}
while tx.witnesses().item_count() + witnesses.len()
@@ -430,8 +459,12 @@ pub fn balance_tx_capacity(
let tx_size = new_tx.data().as_reader().serialized_size_in_block();
let min_fee = balancer.fee_rate.fee(tx_size).as_u64();
let mut need_more_capacity = 1;
- let fee_result: Result =
- tx_fee(new_tx.clone(), tx_dep_provider, header_dep_resolver);
+ let fee_result: Result = tx_fee_with_open(
+ new_tx.clone(),
+ tx_dep_provider,
+ header_dep_resolver,
+ open_capacity,
+ );
match fee_result {
Ok(fee) if fee == min_fee => {
return Ok(new_tx);
diff --git a/src/tx_builder/omni_lock.rs b/src/tx_builder/omni_lock.rs
index 2662f030..ac5d0607 100644
--- a/src/tx_builder/omni_lock.rs
+++ b/src/tx_builder/omni_lock.rs
@@ -1,24 +1,28 @@
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
use ckb_types::{
bytes::Bytes,
core::{DepType, TransactionBuilder, TransactionView},
- packed::{CellDep, CellInput, CellOutput, OutPoint},
+ packed::{CellDep, CellInput, CellOutput, OutPoint, Script},
prelude::*,
};
-use super::{TxBuilder, TxBuilderError};
-use crate::types::ScriptId;
+use super::{
+ balance_tx_capacity_with_open, fill_placeholder_witnesses, CapacityBalancer, TxBuilder,
+ TxBuilderError,
+};
use crate::{
traits::{CellCollector, CellDepResolver, HeaderDepResolver, TransactionDependencyProvider},
- unlock::OmniLockConfig,
+ unlock::{omni_lock::ConfigError, OmniLockConfig, OmniUnlockMode, ScriptUnlocker},
};
+use crate::{types::ScriptId, HumanCapacity};
/// A builder to build an omnilock transfer transaction.
pub struct OmniLockTransferBuilder {
pub outputs: Vec<(CellOutput, Bytes)>,
pub cfg: OmniLockConfig,
pub rce_cells: Option>,
+ pub open_out_capacity: HumanCapacity,
}
impl OmniLockTransferBuilder {
@@ -31,8 +35,57 @@ impl OmniLockTransferBuilder {
outputs,
cfg,
rce_cells,
+ open_out_capacity: HumanCapacity(0),
+ }
+ }
+
+ /// Create an OmniLockTransferBuilder with open out in the output list.
+ /// After the transaction built, the open out should be removed.
+ pub fn new_open(
+ open_out_capacity: HumanCapacity,
+ outputs: Vec<(CellOutput, Bytes)>,
+ cfg: OmniLockConfig,
+ rce_cells: Option>,
+ ) -> OmniLockTransferBuilder {
+ OmniLockTransferBuilder {
+ outputs,
+ cfg,
+ rce_cells,
+ open_out_capacity,
}
}
+
+ /// after the open transaction input list updated(exclude base input/output), the witness should be updated
+ pub fn update_opentx_witness(
+ tx: TransactionView,
+ omnilock_config: &OmniLockConfig,
+ unlock_mode: OmniUnlockMode,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
+ sender: &Script,
+ ) -> Result {
+ // after set opentx config, need to update the witness field
+ let placeholder_witness = omnilock_config.placeholder_witness(unlock_mode)?;
+ let tmp_idxes: Vec<_> = tx
+ .input_pts_iter()
+ .enumerate()
+ .filter(|(_, output)| tx_dep_provider.get_cell(output).unwrap().lock() == *sender)
+ .map(|(idx, _)| idx)
+ .collect();
+ let witnesses: Vec<_> = tx
+ .witnesses()
+ .into_iter()
+ .enumerate()
+ .map(|(i, w)| {
+ if tmp_idxes.contains(&i) {
+ placeholder_witness.as_bytes().pack()
+ } else {
+ w
+ }
+ })
+ .collect();
+ let tx = tx.as_advanced_builder().set_witnesses(witnesses).build();
+ Ok(tx)
+ }
}
impl TxBuilder for OmniLockTransferBuilder {
@@ -101,4 +154,37 @@ impl TxBuilder for OmniLockTransferBuilder {
.set_outputs_data(outputs_data)
.build())
}
+
+ /// Build balanced transaction that ready to sign:
+ /// * Build base transaction
+ /// * Fill placeholder witness for lock script
+ /// * balance the capacity
+ fn build_balanced(
+ &self,
+ cell_collector: &mut dyn CellCollector,
+ cell_dep_resolver: &dyn CellDepResolver,
+ header_dep_resolver: &dyn HeaderDepResolver,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
+ balancer: &CapacityBalancer,
+ unlockers: &HashMap>,
+ ) -> Result {
+ let base_tx = self.build_base(
+ cell_collector,
+ cell_dep_resolver,
+ header_dep_resolver,
+ tx_dep_provider,
+ )?;
+ let (tx_filled_witnesses, _) =
+ fill_placeholder_witnesses(base_tx, tx_dep_provider, unlockers)?;
+ let tx = balance_tx_capacity_with_open(
+ &tx_filled_witnesses,
+ balancer,
+ cell_collector,
+ tx_dep_provider,
+ cell_dep_resolver,
+ header_dep_resolver,
+ self.open_out_capacity.into(),
+ )?;
+ Ok(tx)
+ }
}
diff --git a/src/tx_builder/transfer.rs b/src/tx_builder/transfer.rs
index 7f960e6b..f1053ca0 100644
--- a/src/tx_builder/transfer.rs
+++ b/src/tx_builder/transfer.rs
@@ -17,11 +17,25 @@ use crate::types::ScriptId;
/// will resolve the type script's cell_dep if given.
pub struct CapacityTransferBuilder {
pub outputs: Vec<(CellOutput, Bytes)>,
+ pub transaction: Option,
}
impl CapacityTransferBuilder {
pub fn new(outputs: Vec<(CellOutput, Bytes)>) -> CapacityTransferBuilder {
- CapacityTransferBuilder { outputs }
+ CapacityTransferBuilder {
+ outputs,
+ transaction: None,
+ }
+ }
+
+ pub fn new_with_transaction(
+ outputs: Vec<(CellOutput, Bytes)>,
+ transaction: TransactionView,
+ ) -> CapacityTransferBuilder {
+ CapacityTransferBuilder {
+ outputs,
+ transaction: Some(transaction),
+ }
}
}
@@ -50,10 +64,15 @@ impl TxBuilder for CapacityTransferBuilder {
}
}
}
- Ok(TransactionBuilder::default()
- .set_cell_deps(cell_deps.into_iter().collect())
- .set_outputs(outputs)
- .set_outputs_data(outputs_data)
+ let builder = if let Some(tx) = self.transaction.as_ref() {
+ tx.as_advanced_builder()
+ } else {
+ TransactionBuilder::default()
+ };
+ Ok(builder
+ .cell_deps(cell_deps)
+ .outputs(outputs)
+ .outputs_data(outputs_data)
.build())
}
}
diff --git a/src/tx_builder/udt/mod.rs b/src/tx_builder/udt/mod.rs
index 4c600f1e..93af71d2 100644
--- a/src/tx_builder/udt/mod.rs
+++ b/src/tx_builder/udt/mod.rs
@@ -265,6 +265,57 @@ pub struct UdtTransferBuilder {
/// The transfer receivers
pub receivers: Vec,
+
+ /// The exist transaction
+ pub transaction: Option,
+}
+
+impl UdtTransferBuilder {
+ pub fn new(type_script: Script, sender: Script, receivers: Vec) -> Self {
+ UdtTransferBuilder {
+ type_script,
+ sender,
+ receivers,
+ transaction: None,
+ }
+ }
+
+ pub fn new_with_transaction(
+ type_script: Script,
+ sender: Script,
+ receivers: Vec,
+ transaction: TransactionView,
+ ) -> Self {
+ UdtTransferBuilder {
+ type_script,
+ sender,
+ receivers,
+ transaction: Some(transaction),
+ }
+ }
+
+ fn tx_output_amount(&self) -> u128 {
+ if self.transaction.is_none() {
+ return 0;
+ }
+ let tx = self.transaction.as_ref().unwrap();
+ tx.outputs_with_data_iter()
+ .filter_map(|(output, data)| {
+ if data.len() >= 16
+ && output.type_().is_some()
+ && output.type_().to_opt().unwrap() == self.type_script
+ // && output.lock() == self.sender
+ {
+ let mut amount_bytes = [0u8; 16];
+ amount_bytes.copy_from_slice(&data.as_ref()[0..16]);
+ let amount = u128::from_le_bytes(amount_bytes);
+ Some(amount)
+ } else {
+ None
+ }
+ })
+ .sum()
+ }
}
impl TxBuilder for UdtTransferBuilder {
@@ -302,6 +353,7 @@ impl TxBuilder for UdtTransferBuilder {
amount_bytes.copy_from_slice(&sender_cell.output_data.as_ref()[0..16]);
let input_total = u128::from_le_bytes(amount_bytes);
let output_total: u128 = self.receivers.iter().map(|receiver| receiver.amount).sum();
+ let output_total = output_total + self.tx_output_amount();
if input_total < output_total {
return Err(TxBuilderError::Other(anyhow!(
"sender udt amount not enough, expected at least: {}, actual: {}",
@@ -334,12 +386,16 @@ impl TxBuilder for UdtTransferBuilder {
outputs.push(output);
outputs_data.push(output_data.pack());
}
-
- Ok(TransactionBuilder::default()
- .set_cell_deps(cell_deps.into_iter().collect())
- .set_inputs(inputs)
- .set_outputs(outputs)
- .set_outputs_data(outputs_data)
+ let builder = if let Some(tx) = self.transaction.as_ref() {
+ tx.as_advanced_builder()
+ } else {
+ TransactionBuilder::default()
+ };
+ Ok(builder
+ .cell_deps(cell_deps)
+ .inputs(inputs)
+ .outputs(outputs)
+ .outputs_data(outputs_data)
.build())
}
}
diff --git a/src/unlock/mod.rs b/src/unlock/mod.rs
index edda5d0d..9cc2cdc1 100644
--- a/src/unlock/mod.rs
+++ b/src/unlock/mod.rs
@@ -1,4 +1,5 @@
pub(crate) mod omni_lock;
+pub mod opentx;
pub mod rc_data;
mod signer;
mod unlocker;
diff --git a/src/unlock/omni_lock.rs b/src/unlock/omni_lock.rs
index 12a31720..86ba6af3 100644
--- a/src/unlock/omni_lock.rs
+++ b/src/unlock/omni_lock.rs
@@ -2,6 +2,7 @@ use core::hash;
use std::fmt::Display;
use crate::{
+ constants::SECP_SIGNATURE_SIZE,
tx_builder::SinceSource,
types::{
omni_lock::{Auth, Identity as IdentityType, IdentityOpt, OmniLockWitnessLock},
@@ -22,7 +23,7 @@ use std::convert::TryFrom;
use bitflags::bitflags;
-use super::{MultisigConfig, OmniUnlockMode};
+use super::{opentx::OpentxWitness, MultisigConfig, OmniUnlockMode};
use thiserror::Error;
#[derive(
@@ -196,6 +197,8 @@ bitflags! {
const TIME_LOCK = 1<<2;
/// supply mode, flag is 1<<3, affected args: type script hash for supply
const SUPPLY = 1<<3;
+ /// open transaction mode.
+ const OPENTX = 1<<4;
}
}
@@ -419,6 +422,11 @@ pub struct OmniLockConfig {
time_lock_config: Option,
// 32 bytes type script hash
info_cell: Option,
+
+ /// open tx config
+ opentx_input: Option,
+ /// When do placeholder_witness_lock, opentx_input will be used if it's not None, or this field is used to reserve more capacity.
+ opentx_reserve_bytes: u32,
}
impl OmniLockConfig {
@@ -442,6 +450,8 @@ impl OmniLockConfig {
acp_config: None,
time_lock_config: None,
info_cell: None,
+ opentx_input: None,
+ opentx_reserve_bytes: 0,
}
}
/// Create an ethereum algorithm omnilock with pubkey
@@ -488,6 +498,8 @@ impl OmniLockConfig {
acp_config: None,
time_lock_config: None,
info_cell: None,
+ opentx_input: None,
+ opentx_reserve_bytes: 0,
}
}
@@ -539,6 +551,48 @@ impl OmniLockConfig {
self.info_cell = None;
}
+ /// Set the open transaction input data, and set the OmniLockFlags::OPENTX to omni_lock_flags.
+ pub fn set_opentx_input(&mut self, opentx_input: OpentxWitness) {
+ self.omni_lock_flags.set(OmniLockFlags::OPENTX, true);
+ self.opentx_input = Some(opentx_input);
+ }
+
+ /// Set the opentx mode, without knowing the open transaction input data yet, can be used to generate lock script args to search avaiable live cells.
+ pub fn set_opentx_mode(&mut self) {
+ self.omni_lock_flags.set(OmniLockFlags::OPENTX, true);
+ }
+
+ /// Check if it contains open transaction mode
+ pub fn is_opentx_mode(&self) -> bool {
+ self.omni_lock_flags.contains(OmniLockFlags::OPENTX)
+ }
+
+ /// Clear the open transaction input data, and clear OmniLockFlags::OPENTX from omni_lock_flags.
+ pub fn clear_opentx_input(&mut self) {
+ self.omni_lock_flags.set(OmniLockFlags::OPENTX, false);
+ self.opentx_input = None;
+ }
+
+ pub fn get_opentx_input(&self) -> Option<&OpentxWitness> {
+ self.opentx_input.as_ref()
+ }
+
+ /// Set opentx reserve bytes for placeholder witness.
+ /// # Arguments
+ /// * `bytes` number of bytes to reserve, the minimual should be 12, and be multiple of 4.
+ pub fn set_opentx_reserve_bytes(&mut self, bytes: u32) {
+ self.opentx_reserve_bytes = bytes;
+ }
+
+ /// Set opentx reserve bytes by possible command numbers.
+ ///
+ /// If all data are hashed, the command number verifies depend on input/output numbers,
+ /// if random salt is used for OpenTxSigInput::new_concat_arg1_arg2, salt bigger than 0xFFFFFF will be 2 commands,
+ /// salt <= 0xFFFFFF will be one 1 command.
+ pub fn set_opentx_reserve_bytes_by_commands(&mut self, commands: u32) {
+ self.set_opentx_reserve_bytes(4 + 4 + 4 * commands);
+ }
+
pub fn id(&self) -> &Identity {
&self.id
}
@@ -650,9 +704,18 @@ impl OmniLockConfig {
&self,
unlock_mode: OmniUnlockMode,
) -> Result {
+ let mut buf = BytesMut::new();
+ if let Some(optx) = self.opentx_input.as_ref() {
+ buf.extend_from_slice(&optx.to_witness_data());
+ } else if self.is_opentx_mode() {
+ let new_len = buf.len() + self.opentx_reserve_bytes as usize;
+ buf.resize(new_len, 0u8);
+ }
let mut builder = match self.id.flag {
- IdentityFlag::PubkeyHash | IdentityFlag::Ethereum => OmniLockWitnessLock::new_builder()
- .signature(Some(Bytes::from(vec![0u8; 65])).pack()),
+ IdentityFlag::PubkeyHash | IdentityFlag::Ethereum => {
+ buf.extend_from_slice(&[0u8; SECP_SIGNATURE_SIZE]);
+ OmniLockWitnessLock::new_builder().signature(Some(buf.freeze()).pack())
+ }
IdentityFlag::Multisig => {
let multisig_config = match unlock_mode {
OmniUnlockMode::Admin => self
@@ -668,10 +731,15 @@ impl OmniLockConfig {
.ok_or(ConfigError::NoMultiSigConfig)?,
};
let config_data = multisig_config.to_witness_data();
- let multisig_len = config_data.len() + multisig_config.threshold() as usize * 65;
- let mut omni_sig = vec![0u8; multisig_len];
- omni_sig[..config_data.len()].copy_from_slice(&config_data);
- OmniLockWitnessLock::new_builder().signature(Some(Bytes::from(omni_sig)).pack())
+ let multisig_len =
+ config_data.len() + multisig_config.threshold() as usize * SECP_SIGNATURE_SIZE;
+ // let mut omni_sig = vec![0u8; multisig_len];
+ // omni_sig[..config_data.len()].copy_from_slice(&config_data);
+ // buf.extend_from_slice(&omni_sig);
+ let offset = buf.len();
+ buf.extend_from_slice(&config_data);
+ buf.resize(offset + multisig_len, 0u8);
+ OmniLockWitnessLock::new_builder().signature(Some(buf.freeze()).pack())
}
IdentityFlag::OwnerLock => OmniLockWitnessLock::new_builder(),
_ => todo!("to support other placeholder_witness_lock implementions"),
diff --git a/src/unlock/opentx/assembler.rs b/src/unlock/opentx/assembler.rs
new file mode 100644
index 00000000..b0d02eec
--- /dev/null
+++ b/src/unlock/opentx/assembler.rs
@@ -0,0 +1,130 @@
+use anyhow::anyhow;
+use std::{cmp::Ordering, collections::HashSet, convert::TryFrom};
+
+use ckb_types::{
+ bytes::Bytes,
+ core::TransactionView,
+ packed::{Byte32, WitnessArgs},
+ prelude::*,
+};
+
+use crate::{traits::TransactionDependencyProvider, unlock::omni_lock::OmniLockFlags};
+use crate::{
+ tx_builder::{gen_script_groups, ScriptGroups},
+ types::omni_lock::OmniLockWitnessLock,
+};
+
+use super::OpenTxError;
+
+/// Check if different
+fn check_script_groups(group_vec: &[ScriptGroups]) -> Result<(), OpenTxError> {
+ let mut keys = HashSet::new();
+ for group in group_vec.iter() {
+ let len = keys.len();
+ keys.extend(group.lock_groups.keys().clone());
+ if len + group.lock_groups.len() > keys.len() {
+ return Err(OpenTxError::SameLockInDifferentOpenTx);
+ }
+ }
+ Ok(())
+}
+
+/// Assemble a transaction from multiple opentransaction, remove duplicate cell deps and header deps.
+/// Alter base input/output index.
+pub fn assemble_new_tx(
+ mut transactions: Vec,
+ provider: &dyn TransactionDependencyProvider,
+ opentx_code_hash: Byte32,
+) -> Result {
+ if transactions.len() == 1 {
+ return Ok(transactions.remove(0));
+ }
+ let mut builder = TransactionView::new_advanced_builder();
+ let mut cell_deps = HashSet::new();
+ let mut header_deps = HashSet::new();
+ let mut base_input_idx = 0usize;
+ let mut base_output_idx = 0usize;
+ let mut base_input_cap = 0usize;
+ let mut base_output_cap = 0usize;
+ let group_vec: Result, _> = transactions
+ .iter()
+ .map(|tx| gen_script_groups(tx, provider))
+ .collect();
+ let group_vec = group_vec?;
+ check_script_groups(&group_vec)?;
+ for tx in transactions.iter() {
+ cell_deps.extend(tx.cell_deps());
+ header_deps.extend(tx.header_deps());
+ builder = builder.inputs(tx.inputs());
+ base_input_cap += tx.inputs().len();
+ base_output_cap += tx.outputs().len();
+ // Handle opentx witness
+ for (input, witness) in tx.inputs().into_iter().zip(tx.witnesses().into_iter()) {
+ let lock = provider.get_cell(&input.previous_output())?.lock();
+ let code_hash = lock.code_hash();
+ // empty witness should be in a script group
+ if !witness.is_empty() && code_hash.cmp(&opentx_code_hash) == Ordering::Equal {
+ let args = &lock.args().raw_data();
+ let witness_data = witness.raw_data();
+ if witness_data.len() > 8 // sizeof base_input + sizeof base_output
+ && args.len() >= 22
+ && OmniLockFlags::from_bits_truncate(args[21]).contains(OmniLockFlags::OPENTX)
+ {
+ // Parse lock data
+ let current_witness: WitnessArgs =
+ WitnessArgs::from_slice(witness_data.as_ref())?;
+ let lock_field = current_witness
+ .lock()
+ .to_opt()
+ .map(|data| data.raw_data())
+ .ok_or(OpenTxError::WitnessLockMissing)?;
+ let omnilock_witnesslock =
+ OmniLockWitnessLock::from_slice(lock_field.as_ref())?;
+
+ let mut data = omnilock_witnesslock
+ .signature()
+ .to_opt()
+ .map(|data| data.raw_data().as_ref().to_vec())
+ .ok_or(OpenTxError::SignatureMissing)?;
+
+ let mut tmp = [0u8; 4];
+ tmp.copy_from_slice(&data[0..4]);
+ let this_base_input_idx = u32::from_le_bytes(tmp)
+ + u32::try_from(base_input_idx).map_err(|e| anyhow!(e))?;
+ if this_base_input_idx as usize > base_input_cap {
+ return Err(OpenTxError::BaseInputIndexOverFlow);
+ }
+ data[0..4].copy_from_slice(&this_base_input_idx.to_le_bytes());
+
+ tmp.copy_from_slice(&data[4..8]);
+ let this_base_output_idx = u32::from_le_bytes(tmp)
+ + u32::try_from(base_output_idx).map_err(|e| anyhow!(e))?;
+ if this_base_output_idx as usize > base_output_cap {
+ return Err(OpenTxError::BaseOutputIndexOverFlow);
+ }
+ data[4..8].copy_from_slice(&this_base_output_idx.to_le_bytes());
+
+ let omnilock_witnesslock = omnilock_witnesslock
+ .as_builder()
+ .signature(Some(Bytes::from(data)).pack())
+ .build();
+ let witness = current_witness
+ .as_builder()
+ .lock(Some(omnilock_witnesslock.as_bytes()).pack())
+ .build();
+ builder = builder.witness(witness.as_bytes().pack());
+ continue;
+ }
+ }
+ builder = builder.witness(witness);
+ }
+ builder = builder.outputs(tx.outputs());
+ builder = builder.outputs_data(tx.outputs_data());
+
+ base_input_idx += tx.inputs().len();
+ base_output_idx += tx.outputs().len();
+ }
+ builder = builder.cell_deps(cell_deps).header_deps(header_deps);
+
+ Ok(builder.build())
+}
diff --git a/src/unlock/opentx/hasher.rs b/src/unlock/opentx/hasher.rs
new file mode 100644
index 00000000..7d986d14
--- /dev/null
+++ b/src/unlock/opentx/hasher.rs
@@ -0,0 +1,716 @@
+use anyhow::anyhow;
+use bitflags::bitflags;
+use bytes::{BufMut, Bytes};
+use ckb_hash::Blake2b;
+use ckb_types::{bytes::BytesMut, core::TransactionView, prelude::*};
+use serde::{Deserialize, Serialize};
+use std::convert::TryFrom;
+
+use enum_repr_derive::{FromEnumToRepr, TryFromReprToEnum};
+
+use super::reader::{OpenTxCellField, OpenTxReader, OpenTxSource};
+use super::OpenTxError;
+
+const ARG1_MASK: u16 = 0xFFF;
+const ARG2_MASK: u16 = 0xFFF;
+
+/// Open transaction signature input command.
+#[derive(
+ Clone,
+ Copy,
+ Serialize,
+ Deserialize,
+ Debug,
+ Hash,
+ Eq,
+ PartialEq,
+ TryFromReprToEnum,
+ FromEnumToRepr,
+)]
+#[repr(u8)]
+pub enum OpentxCommand {
+ /// Hash the full current transaction hash
+ TxHash = 0x00,
+ /// Hash length of input & output cells in current script group
+ GroupInputOutputLen = 0x01,
+ /// Hash part or the whole output cell. arg1 is index of output cell, arg2 is cell mask.
+ IndexOutput = 0x11,
+ /// Hash part or the whole output cell. arg1 is offset of output cell, arg2 is cell mask.
+ OffsetOutput = 0x12,
+ /// Hash part or the whole input cell. arg1 is index of input cell, arg2 is cell mask.
+ IndexInput = 0x13,
+ /// Hash part or the whole input cell. arg1 is offset of input cell, arg2 is cell mask.
+ OffsetInput = 0x14,
+ /// Hash part or the whole cell input structure, arg1 is index of input cell, arg2 is input mask.
+ CellInputIndex = 0x15,
+ /// Hash part or the whole cell input structure, arg1 is offset of input cell, arg2 is input mask.`
+ CellInputOffset = 0x16,
+ /// Concatenate ARG 1 and ARG 2, arg1 is lower 12 bit, arg2 is higher 12 bit.
+ /// The purpose of this command is to add salt for hash.
+ ConcatArg1Arg2 = 0x20,
+ /// Terminate and generate the final blake2b hash
+ End = 0xF0,
+}
+
+impl OpentxCommand {
+ pub fn is_index(&self) -> bool {
+ matches!(
+ self,
+ OpentxCommand::IndexOutput | OpentxCommand::IndexInput | OpentxCommand::CellInputIndex
+ )
+ }
+}
+
+bitflags! {
+ /// The bits control the data to generate from a cell.
+ #[derive(Serialize, Deserialize)]
+ pub struct CellMask: u16 {
+ /// capacity
+ const CAPACITY = 0x1;
+ /// lock.code_hash
+ const LOCK_CODE_HASH = 0x2;
+ /// lock.hash_type
+ const LOCK_HASH_TYPE = 0x4;
+ /// lock.args
+ const LOCK_ARGS = 0x8;
+ /// type.code_hash
+ const TYPE_CODE_HASH = 0x10;
+ /// type.hash_type
+ const TYPE_HASH_TYPE = 0x20;
+ /// type.args
+ const TYPE_ARGS = 0x40;
+ /// Cell data
+ const CELL_DATA = 0x80;
+ /// Lock script hash
+ const LOCK_SCRIPT_HASH = 0x100;
+ /// Type script hash
+ const TYPE_SCRIPT_HASH = 0x200;
+ /// The whole cell
+ const WHOLE_CELL = 0x400;
+ }
+}
+
+bitflags! {
+ /// The bits control the data to generate from a CellInput structure.
+ #[derive(Serialize, Deserialize)]
+ pub struct InputMask: u16 {
+ /// previous_output.tx_hash
+ const TX_HASH = 0x1;
+ /// previous_output.index
+ const INDEX = 0x2;
+ /// since
+ const SINCE = 0x4;
+ /// previous_output
+ const PREVIOUS_OUTPUT = 0x8;
+ /// The whole CellInput structure
+ const WHOLE = 0x10;
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq)]
+pub struct OpenTxSigInput {
+ pub cmd: OpentxCommand,
+ pub arg1: u16,
+ pub arg2: u16,
+}
+
+impl OpenTxSigInput {
+ pub fn compose(&self) -> u32 {
+ (self.cmd as u32)
+ + (((self.arg1 & ARG1_MASK) as u32) << 8)
+ + (((self.arg2 & ARG2_MASK) as u32) << 20)
+ }
+
+ /// Build new OpentxCommand::TxHash OpenTxSigInput, command 0x00
+ pub fn new_tx_hash() -> OpenTxSigInput {
+ OpenTxSigInput {
+ cmd: OpentxCommand::TxHash,
+ arg1: 0,
+ arg2: 0,
+ }
+ }
+ // Build new OpentxCommand::GroupInputOutputLen OpenTxSigInput, command 0x01
+ pub fn new_group_input_output_len() -> OpenTxSigInput {
+ OpenTxSigInput {
+ cmd: OpentxCommand::GroupInputOutputLen,
+ arg1: 0,
+ arg2: 0,
+ }
+ }
+ /// Build new OpentxCommand::IndexOutput OpenTxSigInput, command 0x11
+ pub fn new_index_output(arg1: u16, arg2: CellMask) -> Result {
+ Self::new_cell_command(OpentxCommand::IndexOutput, arg1, arg2)
+ }
+ /// Build new OpentxCommand::OffsetOutput OpenTxSigInput, command 0x12
+ pub fn new_offset_output(arg1: u16, arg2: CellMask) -> Result {
+ Self::new_cell_command(OpentxCommand::OffsetOutput, arg1, arg2)
+ }
+ /// Build new OpentxCommand::IndexInput OpenTxSigInput, command 0x13
+ pub fn new_index_input(arg1: u16, arg2: CellMask) -> Result {
+ Self::new_cell_command(OpentxCommand::IndexInput, arg1, arg2)
+ }
+ /// Build new OpentxCommand::OffsetInput OpenTxSigInput, command 0x14
+ pub fn new_offset_input(arg1: u16, arg2: CellMask) -> Result {
+ Self::new_cell_command(OpentxCommand::OffsetInput, arg1, arg2)
+ }
+ /// Build new OpenTxSigInput to handle part or the whole input/output cell
+ pub fn new_cell_command(
+ cmd: OpentxCommand,
+ arg1: u16,
+ arg2: CellMask,
+ ) -> Result {
+ if arg1 > ARG1_MASK {
+ return Err(OpenTxError::Arg1OutOfRange(arg1));
+ }
+
+ Ok(OpenTxSigInput {
+ cmd,
+ arg1,
+ arg2: arg2.bits,
+ })
+ }
+ /// Build new OpentxCommand::CellInputIndex OpenTxSigInput, command 0x15
+ pub fn new_cell_input_index(arg1: u16, arg2: InputMask) -> Result {
+ Self::new_input_command(OpentxCommand::CellInputIndex, arg1, arg2)
+ }
+ //// Build new OpentxCommand::CellInputOffset OpenTxSigInput, command 0x16
+ pub fn new_cell_input_offset(
+ arg1: u16,
+ arg2: InputMask,
+ ) -> Result {
+ Self::new_input_command(OpentxCommand::CellInputOffset, arg1, arg2)
+ }
+ /// Build new OpenTxSigInput to hash part or the whole cell input structure
+ pub fn new_input_command(
+ cmd: OpentxCommand,
+ arg1: u16,
+ arg2: InputMask,
+ ) -> Result {
+ if arg1 > ARG1_MASK {
+ return Err(OpenTxError::Arg1OutOfRange(arg1));
+ }
+
+ Ok(OpenTxSigInput {
+ cmd,
+ arg1,
+ arg2: arg2.bits,
+ })
+ }
+
+ /// Build new OpentxCommand::ConcatArg1Arg2 OpenTxSigInput, command 0x20
+ pub fn new_concat_arg1_arg2(arg1: u16, arg2: u16) -> OpenTxSigInput {
+ OpenTxSigInput {
+ cmd: OpentxCommand::ConcatArg1Arg2,
+ arg1: arg1 & ARG1_MASK,
+ arg2: arg2 & ARG2_MASK,
+ }
+ }
+ /// Build new OpentxCommand::End OpenTxSigInput, command 0xF0
+ pub fn new_end() -> OpenTxSigInput {
+ OpenTxSigInput {
+ cmd: OpentxCommand::End,
+ arg1: 0,
+ arg2: 0,
+ }
+ }
+ fn hash_cell(
+ &self,
+ cache: &mut OpentxCache,
+ reader: &OpenTxReader,
+ is_input: bool,
+ with_offset: bool,
+ base_index: u32,
+ ) -> Result<(), OpenTxError> {
+ let mut index = self.arg1 as usize;
+ if with_offset {
+ index += base_index as usize;
+ }
+ let source = if is_input {
+ OpenTxSource::Input
+ } else {
+ OpenTxSource::Outpout
+ };
+ let cell_mask = CellMask::from_bits_truncate(self.arg2);
+ if cell_mask.contains(CellMask::CAPACITY) {
+ let data = reader.load_cell_field(index, source, OpenTxCellField::Capacity)?;
+ cache.update(&data);
+ }
+ if cell_mask.intersects(
+ CellMask::LOCK_CODE_HASH
+ | CellMask::LOCK_HASH_TYPE
+ | CellMask::LOCK_ARGS
+ | CellMask::TYPE_CODE_HASH
+ | CellMask::TYPE_HASH_TYPE
+ | CellMask::TYPE_ARGS,
+ ) {
+ let cell = reader.get_cell(index, is_input)?;
+ let lock = cell.lock();
+ if cell_mask.contains(CellMask::LOCK_CODE_HASH) {
+ cache.update(lock.code_hash().as_slice());
+ }
+ if cell_mask.contains(CellMask::LOCK_HASH_TYPE) {
+ cache.update(lock.hash_type().as_slice());
+ }
+ if cell_mask.contains(CellMask::LOCK_ARGS) {
+ let args = lock.args().raw_data().to_vec();
+ cache.update(args.as_slice());
+ }
+
+ if let Some(type_) = cell.type_().to_opt() {
+ if cell_mask.contains(CellMask::TYPE_CODE_HASH) {
+ cache.update(type_.code_hash().as_slice());
+ }
+ if cell_mask.contains(CellMask::TYPE_HASH_TYPE) {
+ cache.update(type_.hash_type().as_slice());
+ }
+ if cell_mask.contains(CellMask::TYPE_ARGS) {
+ let args = type_.args().raw_data().to_vec();
+ cache.update(&args);
+ }
+ }
+ }
+ if cell_mask.contains(CellMask::CELL_DATA) {
+ let data = reader.load_cell_data(index, source)?;
+ cache.update(data.as_slice());
+ }
+
+ if cell_mask.contains(CellMask::LOCK_SCRIPT_HASH) {
+ let cell = reader.get_cell(index, is_input)?;
+ let hash = cell.lock().calc_script_hash();
+ cache.update(hash.as_slice());
+ }
+
+ if cell_mask.contains(CellMask::TYPE_SCRIPT_HASH) {
+ let cell = reader.get_cell(index, is_input)?;
+ if let Some(script) = cell.type_().to_opt() {
+ let hash = script.calc_script_hash();
+ cache.update(hash.as_slice());
+ }
+ }
+
+ if cell_mask.contains(CellMask::WHOLE_CELL) {
+ let data = reader.load_cell(index, source)?;
+ cache.update(data.as_slice());
+ }
+ Result::Ok(())
+ }
+
+ fn hash_input(
+ &self,
+ cache: &mut OpentxCache,
+ reader: &OpenTxReader,
+ with_offset: bool,
+ base_index: u32,
+ ) -> Result<(), OpenTxError> {
+ let index = if with_offset {
+ usize::try_from(base_index)
+ .map_err(|e| anyhow!(e))?
+ .checked_add(self.arg1 as usize)
+ .ok_or_else(|| anyhow!("add {} and {} overflow", base_index, self.arg1))?
+ } else {
+ self.arg1 as usize
+ };
+
+ let input_mask = InputMask::from_bits_truncate(self.arg2);
+ if input_mask.contains(InputMask::TX_HASH) {
+ let cell = reader.input(index)?;
+ let data = cell.previous_output().tx_hash();
+ cache.update(data.as_slice());
+ }
+
+ if input_mask.contains(InputMask::INDEX) {
+ let cell = reader.input(index)?;
+ let data = cell.previous_output().index();
+ cache.update(data.as_slice());
+ }
+
+ if input_mask.contains(InputMask::SINCE) {
+ let data = reader.load_input_field_since(index)?;
+
+ cache.update(&data);
+ }
+
+ if input_mask.contains(InputMask::PREVIOUS_OUTPUT) {
+ let data = reader.load_input_field_out_point(index)?;
+
+ cache.update(&data);
+ }
+
+ if input_mask.contains(InputMask::WHOLE) {
+ let data = reader.load_input(index)?;
+ cache.update(&data);
+ }
+ Ok(())
+ }
+}
+#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq)]
+pub struct OpentxWitness {
+ pub base_input_index: u32,
+ pub base_output_index: u32,
+ pub inputs: Vec,
+}
+
+impl OpentxWitness {
+ pub fn new_empty() -> Self {
+ OpentxWitness {
+ base_input_index: 0,
+ base_output_index: 0,
+ inputs: vec![],
+ }
+ }
+ pub fn new(base_input_index: u32, base_output_index: u32, inputs: Vec) -> Self {
+ OpentxWitness {
+ base_input_index,
+ base_output_index,
+ inputs,
+ }
+ }
+
+ /// Build new OpentxWitness which will sign all data.
+ ///
+ /// It will first generate the GroupInputOutputLen(0x01),
+ /// then iterate the inputs to generate the relative index OpenTxSigInput with all CellMask on, and InputMask on,
+ /// then iterate each output to generate the relative index OpenTxSigInput with all CellMask on.
+ ///
+ /// If salt provided, it will add low 24 bits with ConcatArg1Arg2(0x20),
+ /// if high 8 bits not all 0, it will add another ConcatArg1Arg2 OpenTxSigInput.
+ ///
+ /// Then it will add an End(0xF0) OpenTxSigInput.
+ ///
+ /// The range of inputs/outputs are [base_input_index, end_input_index), [base_output_index, end_output_index).
+ /// If end_input_index bigger than the transaction inputs length, the inputs length will be used, same thing to end_output_index.
+ ///
+ pub fn new_sig_range_relative(
+ transaction: &TransactionView,
+ salt: Option,
+ base_input_index: usize,
+ end_input_index: usize,
+ base_output_index: usize,
+ end_output_index: usize,
+ ) -> Result {
+ let mut inputs = vec![
+ // If the opentx will add other input/output, when sign, it can not get the correct transaction hash,
+ // so OpentxCommand::TxHash should not be included in an opentx will add more input(s)/output(s)
+ // OpenTxSigInput::new_tx_hash(),
+ OpenTxSigInput::new_group_input_output_len(),
+ ];
+
+ let start_input_idx = base_input_index;
+ let end_input_idx = end_input_index.min(transaction.inputs().len());
+ if start_input_idx >= end_input_idx {
+ return Err(OpenTxError::BaseIndexOverFlow(
+ start_input_idx,
+ end_input_idx,
+ ));
+ }
+
+ let start_output_idx = base_output_index;
+ let out_put_idx = end_output_index.min(transaction.outputs().len());
+ if start_output_idx >= out_put_idx {
+ return Err(OpenTxError::BaseIndexOverFlow(
+ start_output_idx,
+ out_put_idx,
+ ));
+ }
+ let base_input_index = u32::try_from(base_input_index).map_err(|e| anyhow!(e))?;
+ let base_output_index = u32::try_from(base_output_index).map_err(|e| anyhow!(e))?;
+ for input_idx in start_input_idx..end_input_idx {
+ let idx = u16::try_from(input_idx - start_input_idx).map_err(|e| anyhow!(e))?;
+ inputs.push(OpenTxSigInput::new_offset_input(
+ idx as u16,
+ CellMask::all(),
+ )?);
+ inputs.push(OpenTxSigInput::new_cell_input_offset(
+ idx as u16,
+ InputMask::all(),
+ )?);
+ }
+ for output_idx in start_output_idx..out_put_idx {
+ let idx = u16::try_from(output_idx - start_output_idx).map_err(|e| anyhow!(e))?;
+ inputs.push(OpenTxSigInput::new_offset_output(
+ idx as u16,
+ CellMask::all(),
+ )?);
+ }
+ if let Some(mut salt) = salt {
+ while salt > 0 {
+ inputs.push(OpenTxSigInput::new_concat_arg1_arg2(
+ salt as u16 & ARG1_MASK,
+ (salt >> 12) as u16 & ARG2_MASK,
+ ));
+ salt >>= 24;
+ }
+ }
+
+ inputs.push(OpenTxSigInput::new_end());
+ Ok(OpentxWitness::new(
+ base_input_index,
+ base_output_index,
+ inputs,
+ ))
+ }
+ /// Same to `new_sig_range_relative`, except end_input_index and end_output_index are all usize::MAX,
+ /// which will be changed to the length of inputs/outputs list.
+ pub fn new_sig_to_end_relative(
+ transaction: &TransactionView,
+ salt: Option,
+ base_input_index: usize,
+ base_output_index: usize,
+ ) -> Result {
+ Self::new_sig_range_relative(
+ transaction,
+ salt,
+ base_input_index,
+ usize::MAX,
+ base_output_index,
+ usize::MAX,
+ )
+ }
+
+ /// Same to `new_sig_to_end_relative`, except base_input_index and base_output_index are all 0
+ pub fn new_sig_all_relative(
+ transaction: &TransactionView,
+ salt: Option,
+ ) -> Result {
+ Self::new_sig_range_relative(transaction, salt, 0, usize::MAX, 0, usize::MAX)
+ }
+
+ /// Same to new_sig_to_end_relative, but the will use the index commands.
+ /// The length will be limit to 0x1000, index range:[0, 4095].
+ pub fn new_sig_range_absolute(
+ transaction: &TransactionView,
+ salt: Option,
+ base_input_index: usize,
+ end_input_index: usize,
+ base_output_index: usize,
+ end_output_index: usize,
+ ) -> Result {
+ let mut inputs = vec![OpenTxSigInput::new_group_input_output_len()];
+
+ let start_input_idx = base_input_index;
+ let end_input_idx =
+ ((ARG1_MASK + 1) as usize).min(end_input_index.min(transaction.inputs().len()));
+ if start_input_idx >= end_input_idx {
+ return Err(OpenTxError::BaseIndexOverFlow(
+ start_input_idx,
+ end_input_idx,
+ ));
+ }
+
+ let start_output_idx = base_output_index;
+ let out_put_idx =
+ ((ARG1_MASK + 1) as usize).min(end_output_index.min(transaction.outputs().len()));
+ if start_output_idx >= out_put_idx {
+ return Err(OpenTxError::BaseIndexOverFlow(
+ start_output_idx,
+ out_put_idx,
+ ));
+ }
+ let base_input_index = u32::try_from(base_input_index).map_err(|e| anyhow!(e))?;
+ let base_output_index = u32::try_from(base_output_index).map_err(|e| anyhow!(e))?;
+ for input_idx in start_input_idx..end_input_idx {
+ inputs.push(OpenTxSigInput::new_index_input(
+ input_idx as u16,
+ CellMask::all(),
+ )?);
+ inputs.push(OpenTxSigInput::new_cell_input_index(
+ input_idx as u16,
+ InputMask::all(),
+ )?);
+ }
+ for output_idx in start_output_idx..out_put_idx {
+ inputs.push(OpenTxSigInput::new_index_output(
+ output_idx as u16,
+ CellMask::all(),
+ )?);
+ }
+ if let Some(mut salt) = salt {
+ while salt > 0 {
+ inputs.push(OpenTxSigInput::new_concat_arg1_arg2(
+ salt as u16 & ARG1_MASK,
+ (salt >> 12) as u16 & ARG2_MASK,
+ ));
+ salt >>= 24;
+ }
+ }
+
+ inputs.push(OpenTxSigInput::new_end());
+ Ok(OpentxWitness::new(
+ base_input_index,
+ base_output_index,
+ inputs,
+ ))
+ }
+ /// Same to `new_sig_range_absolute`, except end_input_index and end_output_index are all usize::MAX,
+ /// which will be changed to the length of inputs/outputs list.
+ pub fn new_sig_to_end_absolute(
+ transaction: &TransactionView,
+ salt: Option,
+ base_input_index: usize,
+ base_output_index: usize,
+ ) -> Result {
+ Self::new_sig_range_absolute(
+ transaction,
+ salt,
+ base_input_index,
+ usize::MAX,
+ base_output_index,
+ usize::MAX,
+ )
+ }
+
+ /// Same to `new_sig_to_end_absolute`, except base_input_index and base_output_index are all 0
+ pub fn new_sig_all_absolute(
+ transaction: &TransactionView,
+ salt: Option,
+ ) -> Result {
+ Self::new_sig_range_absolute(transaction, salt, 0, usize::MAX, 0, usize::MAX)
+ }
+
+ /// Add OpentxCommand to the first of the input list, this should only be called if the opentx is ready to sent.
+ /// If the open transaction will be add input/output, this function should not be called.
+ pub fn add_tx_hash_input(&mut self) {
+ self.inputs.insert(0, OpenTxSigInput::new_tx_hash());
+ }
+
+ pub fn set_base_input_index(&mut self, index: u32) {
+ self.base_input_index = index;
+ }
+
+ pub fn set_base_output_index(&mut self, index: u32) {
+ self.base_output_index = index;
+ }
+
+ pub fn to_witness_data(&self) -> Vec {
+ let capacity = self.opentx_sig_data_len();
+ let mut witness_data = Vec::with_capacity(capacity);
+ witness_data.extend_from_slice(&self.base_input_index.to_le_bytes());
+ witness_data.extend_from_slice(&self.base_output_index.to_le_bytes());
+ for inpt in &self.inputs {
+ witness_data.extend_from_slice(&inpt.compose().to_le_bytes());
+ }
+ witness_data
+ }
+
+ /// Generate message for sign.
+ ///
+ /// # Arguments
+ /// * `reader` the read object can fetch data from blockchain.
+ ///
+ /// Return a tuple with first one is the message, the second is open transaction data exclude signature in witness.
+ pub fn generate_message(&self, reader: &OpenTxReader) -> Result<(Bytes, Bytes), OpenTxError> {
+ let (is_input, is_output) = (true, false);
+ let (relative_idx, absolute_idx) = (true, false);
+
+ let mut cache = OpentxCache::new();
+ let mut s_data = BytesMut::with_capacity(self.opentx_sig_data_len());
+ s_data.put_u32_le(self.base_input_index as u32);
+ s_data.put_u32_le(self.base_output_index as u32);
+ let mut has_last = false;
+ for si in &self.inputs {
+ match si.cmd {
+ OpentxCommand::TxHash => {
+ let tx_hash = reader.tx_hash();
+ cache.update(tx_hash.as_slice());
+ }
+ OpentxCommand::GroupInputOutputLen => {
+ let input_len = reader.group_input_len()?;
+ cache.update(&input_len.to_le_bytes());
+ let output_len = 0u64;
+ cache.update(&output_len.to_le_bytes());
+ }
+ OpentxCommand::IndexOutput => {
+ si.hash_cell(&mut cache, reader, is_output, absolute_idx, 0)?;
+ }
+ OpentxCommand::OffsetOutput => {
+ si.hash_cell(
+ &mut cache,
+ reader,
+ is_output,
+ relative_idx,
+ self.base_output_index,
+ )?;
+ }
+ OpentxCommand::IndexInput => {
+ si.hash_cell(&mut cache, reader, is_input, absolute_idx, 0)?;
+ }
+ OpentxCommand::OffsetInput => {
+ si.hash_cell(
+ &mut cache,
+ reader,
+ is_input,
+ relative_idx,
+ self.base_input_index,
+ )?;
+ }
+ OpentxCommand::CellInputIndex => {
+ si.hash_input(&mut cache, reader, absolute_idx, 0)?;
+ }
+ OpentxCommand::CellInputOffset => {
+ si.hash_input(&mut cache, reader, is_input, self.base_input_index)?;
+ }
+ OpentxCommand::ConcatArg1Arg2 => {
+ let data = (si.arg1 as u32 & 0xfff) | ((si.arg2 as u32 & 0xfff) << 12);
+ let data = data.to_le_bytes();
+ cache.update(&data[0..3]);
+ }
+ OpentxCommand::End => {
+ has_last = true;
+ s_data.extend_from_slice(&si.compose().to_le_bytes());
+ break;
+ }
+ }
+ s_data.extend_from_slice(&si.compose().to_le_bytes());
+ }
+ // append last end command
+ if !has_last {
+ let si = OpenTxSigInput::new_end();
+ s_data.extend_from_slice(&si.compose().to_le_bytes());
+ }
+ let s_data = s_data.freeze();
+ cache.update(s_data[8..].to_vec().as_slice());
+
+ let msg = cache.finalize();
+ Ok((msg, s_data))
+ }
+
+ /// The byte length of base_input_index, base_output_index, and signature input list in the witness field.
+ pub fn opentx_sig_data_len(&self) -> usize {
+ 4 + 4 + 4 * self.inputs.len()
+ }
+
+ /// Build open transaction signature by combile open_sig_data and real signatures sig_bytes
+ ///
+ /// # Arguments
+ /// * `open_sig_data` open transaction data, include base_input_index, base_output_index, and signature input list.
+ /// * `sig_bytes` real signature bytes.
+ pub fn build_opentx_sig(&self, open_sig_data: Bytes, sig_bytes: Bytes) -> Bytes {
+ let mut data = BytesMut::with_capacity(self.opentx_sig_data_len() + sig_bytes.len());
+
+ data.put(open_sig_data);
+ data.put(sig_bytes);
+ data.freeze()
+ }
+}
+
+struct OpentxCache {
+ blake2b: Blake2b,
+}
+
+impl OpentxCache {
+ pub fn new() -> Self {
+ OpentxCache {
+ blake2b: ckb_hash::new_blake2b(),
+ }
+ }
+
+ pub fn update(&mut self, data: &[u8]) {
+ self.blake2b.update(data);
+ }
+
+ pub fn finalize(self) -> Bytes {
+ let mut msg = vec![0u8; 32];
+ self.blake2b.finalize(&mut msg);
+ Bytes::from(msg)
+ }
+}
diff --git a/src/unlock/opentx/mod.rs b/src/unlock/opentx/mod.rs
new file mode 100644
index 00000000..bcdb68c3
--- /dev/null
+++ b/src/unlock/opentx/mod.rs
@@ -0,0 +1,47 @@
+pub mod assembler;
+pub mod hasher;
+pub mod reader;
+
+use ckb_types::error::VerificationError;
+pub use hasher::OpentxWitness;
+
+use crate::traits::TransactionDependencyError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum OpenTxError {
+ #[error("Transaction read error, index out of bound.")]
+ OutOfBound,
+ #[error("Item not exist")]
+ ItemMissing,
+ #[error("Fail to get cell `{0}`")]
+ CellNotExist(#[from] TransactionDependencyError),
+ #[error("Unsupport data source")]
+ UnsupportSource,
+ #[error("usize(`{0}`) to u64 overflow.")]
+ LenOverflow(usize),
+
+ #[error("arg1(`{0}`) out of range")]
+ Arg1OutOfRange(u16),
+ #[error("arg2(`{0}`) out of range")]
+ Arg2OutOfRange(u16),
+ #[error("base index(`{0}) bigger than end index(`{1}`)")]
+ BaseIndexOverFlow(usize, usize),
+
+ #[error(transparent)]
+ VerificationError(#[from] VerificationError),
+ #[error("Open transaction input list not configured.")]
+ InputListConfigMissing,
+ #[error("lock field of witness not exist")]
+ WitnessLockMissing,
+ #[error("signature not exist")]
+ SignatureMissing,
+ #[error("Cells in different partital open transaction can not be merged")]
+ SameLockInDifferentOpenTx,
+ #[error("Base input index is overflow")]
+ BaseInputIndexOverFlow,
+ #[error("Base output index is overflow")]
+ BaseOutputIndexOverFlow,
+ #[error(transparent)]
+ Other(#[from] anyhow::Error),
+}
diff --git a/src/unlock/opentx/reader.rs b/src/unlock/opentx/reader.rs
new file mode 100644
index 00000000..baebcbe0
--- /dev/null
+++ b/src/unlock/opentx/reader.rs
@@ -0,0 +1,479 @@
+use std::convert::TryFrom;
+
+use ckb_hash::blake2b_256;
+use ckb_types::{
+ core::{Capacity, TransactionView},
+ packed::{Byte32, CellDep, CellInput, CellOutput, OutPoint, Script},
+ prelude::Entity,
+};
+
+use super::OpenTxError;
+use crate::{traits::TransactionDependencyProvider, ScriptGroup};
+
+#[derive(Copy, Clone, PartialEq)]
+pub enum OpenTxSource {
+ Input,
+ GroupInput,
+ Outpout,
+ CellDep,
+}
+
+#[derive(Copy, Clone)]
+pub enum OpenTxCellField {
+ Capacity,
+ DataHash,
+ Lock,
+ LockHash,
+ Type,
+ TypeHash,
+ OccupiedCapacity,
+}
+
+#[derive(Copy, Clone)]
+pub enum OpenTxInputField {
+ OutPoint,
+ Since,
+}
+
+/// This reader can only read for only one input group, if you have multiple input group, multiple reader will be needed.
+pub struct OpenTxReader<'r> {
+ pub transaction: TransactionView,
+ pub provider: &'r dyn TransactionDependencyProvider,
+ /// map group input index to input index
+ group_input_index: Vec,
+}
+
+impl<'r> OpenTxReader<'r> {
+ pub fn new(
+ transaction: &TransactionView,
+ provider: &'r dyn TransactionDependencyProvider,
+ script_group: &ScriptGroup,
+ ) -> Result {
+ Ok(OpenTxReader {
+ transaction: transaction.clone(),
+ provider,
+ group_input_index: script_group.input_indices.clone(),
+ })
+ }
+
+ /// get the group input length.
+ pub fn group_input_len(&self) -> Result {
+ let len = self.group_input_index.len();
+ let len = u64::try_from(len).map_err(|_e| OpenTxError::LenOverflow(len))?;
+ Ok(len)
+ }
+
+ /// Get input at absolute index
+ pub fn input(&self, index: usize) -> Result {
+ self.transaction
+ .inputs()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)
+ }
+ /// Get previous output of input
+ /// # Arguments
+ /// * `index` absolute index of inputs.
+ fn input_previous_output(&self, index: usize) -> Result {
+ let cell = self.input(index)?;
+ Ok(cell.previous_output())
+ }
+
+ /// Get CellOutput of input's cell
+ /// # Arguments
+ /// * `index` absolute index of inputs.
+ fn input_cell(&self, index: usize) -> Result {
+ let previous_output = self.input_previous_output(index)?;
+ let cell_output = self.provider.get_cell(&previous_output)?;
+ Ok(cell_output)
+ }
+
+ /// Get CellOutput of input's cell
+ /// # Arguments
+ /// * `index` absolute index of inputs.
+ fn group_input_cell(&self, index: usize) -> Result {
+ if self.group_input_index.len() <= index {
+ return Result::Err(OpenTxError::OutOfBound);
+ }
+ let index = self.group_input_index[index];
+ self.input_cell(index)
+ }
+ /// Get cell data of input's cell
+ /// # Arguments
+ /// * `index` absolute index of inputs.
+ fn input_cell_data(&self, index: usize) -> Result {
+ let previous_output = self.input_previous_output(index)?;
+ let cell_data = self.provider.get_cell_data(&previous_output)?;
+ Ok(cell_data)
+ }
+ /// Get cell data of input's cell
+ /// # Arguments
+ /// * `index` absolute index of output.
+ fn output_cell(&self, index: usize) -> Result {
+ self.transaction
+ .output(index)
+ .ok_or(OpenTxError::OutOfBound)
+ }
+ /// Get cell raw data of output's cell
+ /// # Arguments
+ /// * `index` absolute index of output.
+ fn output_cell_data(&self, index: usize) -> Result {
+ Ok(self
+ .transaction
+ .outputs_data()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?
+ .raw_data())
+ }
+ /// Get CellDep of cell depends
+ /// # Arguments
+ /// * `index` absolute index of cell deps.
+ fn cell_dep(&self, index: usize) -> Result {
+ self.transaction
+ .cell_deps()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)
+ }
+ /// Get CellOutput of cell depend
+ /// # Arguments
+ /// * `index` absolute index of cell deps.
+ fn cell_dep_cell(&self, index: usize) -> Result {
+ let outpoint = self.cell_dep(index)?;
+ let cell = self.provider.get_cell(&outpoint.out_point())?;
+ Ok(cell)
+ }
+ fn cell_dep_cell_data(&self, index: usize) -> Result {
+ let outpoint = self.cell_dep(index)?;
+ let cell = self.provider.get_cell_data(&outpoint.out_point())?;
+
+ Ok(cell)
+ }
+ /// Fetch the hash of the current running transaction
+ pub fn tx_hash(&self) -> Byte32 {
+ self.transaction.hash()
+ }
+
+ pub fn load_transaction(&self) -> Vec {
+ self.transaction.data().as_slice().to_vec()
+ }
+
+ pub fn load_cell(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> {
+ let cell = match source {
+ OpenTxSource::Input => self.input_cell(index),
+ OpenTxSource::Outpout => self.output_cell(index),
+ OpenTxSource::CellDep => self.cell_dep_cell(index),
+ _ => Err(OpenTxError::UnsupportSource),
+ };
+ Ok(cell?.as_slice().to_vec())
+ }
+
+ pub fn load_cell_data(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ let data = match source {
+ OpenTxSource::Input => self.input_cell_data(index)?.to_vec(),
+ OpenTxSource::Outpout => self.output_cell_data(index)?.to_vec(),
+ OpenTxSource::CellDep => self.cell_dep_cell_data(index)?.to_vec(),
+ _ => return Err(OpenTxError::UnsupportSource),
+ };
+ Ok(data.to_vec())
+ }
+
+ pub fn load_input(&self, index: usize) -> Result, OpenTxError> {
+ let input = self.input(index)?;
+ Result::Ok(input.as_slice().to_vec())
+ }
+
+ fn load_field_capacity(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ let cell = match source {
+ OpenTxSource::Input => self.input_cell(index)?,
+ OpenTxSource::Outpout => self.output_cell(index)?,
+ OpenTxSource::GroupInput => self.group_input_cell(index)?,
+ OpenTxSource::CellDep => self.cell_dep_cell(index)?,
+ };
+ Ok(cell.capacity().raw_data().to_vec())
+ }
+
+ fn load_field_data_hash(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell_data(index)?;
+
+ let data = input.to_vec();
+ Result::Ok(if data.is_empty() {
+ [0u8; 32].to_vec()
+ } else {
+ blake2b_256(data).to_vec()
+ })
+ }
+ OpenTxSource::Outpout => {
+ let output = self
+ .transaction
+ .outputs_data()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+ let data = output.raw_data().to_vec();
+ if data.is_empty() {
+ Result::Ok([0u8; 32].to_vec())
+ } else {
+ Result::Ok(data)
+ }
+ }
+ OpenTxSource::CellDep => {
+ let outpoint = self.transaction.cell_deps().get(index);
+ if outpoint.is_none() {
+ return Result::Err(OpenTxError::OutOfBound);
+ }
+ let data = self
+ .provider
+ .get_cell_data(&outpoint.unwrap().out_point())?;
+ Result::Ok(if data.is_empty() {
+ [0u8; 32].to_vec()
+ } else {
+ blake2b_256(&data).to_vec()
+ })
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ fn load_field_lock(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell(index)?;
+ Result::Ok(input.lock().as_bytes().to_vec())
+ }
+ OpenTxSource::Outpout => {
+ let output = self
+ .transaction
+ .output(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+ Result::Ok(output.lock().as_bytes().to_vec())
+ }
+ OpenTxSource::CellDep => {
+ let outpoint = self
+ .transaction
+ .cell_deps()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+ let cell = self.provider.get_cell(&outpoint.out_point())?;
+ Result::Ok(cell.lock().as_bytes().to_vec())
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ fn load_field_lock_hash(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell(index)?;
+ Result::Ok(input.calc_lock_hash().as_bytes().to_vec())
+ }
+ OpenTxSource::Outpout => {
+ let output = self
+ .transaction
+ .output(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ Result::Ok(output.calc_lock_hash().as_bytes().to_vec())
+ }
+ OpenTxSource::CellDep => {
+ let outpoint = self
+ .transaction
+ .cell_deps()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ let cell = self.provider.get_cell(&outpoint.out_point())?;
+ Result::Ok(cell.calc_lock_hash().as_bytes().to_vec())
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ fn load_field_type(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell(index)?;
+ let d = input.type_();
+ if d.is_none() {
+ Result::Err(OpenTxError::ItemMissing)
+ } else {
+ Result::Ok(d.as_bytes().to_vec())
+ }
+ }
+ OpenTxSource::Outpout => {
+ let output = self
+ .transaction
+ .output(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ let d = output.type_();
+ if d.is_none() {
+ Result::Err(OpenTxError::ItemMissing)
+ } else {
+ Result::Ok(d.as_bytes().to_vec())
+ }
+ }
+ OpenTxSource::CellDep => {
+ let outpoint = self
+ .transaction
+ .cell_deps()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ let cell = self.provider.get_cell(&outpoint.out_point())?;
+ let d = cell.type_();
+ if d.is_none() {
+ Result::Err(OpenTxError::ItemMissing)
+ } else {
+ Result::Ok(d.as_bytes().to_vec())
+ }
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ fn load_field_type_hash(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell(index)?;
+ let d = input.type_();
+ if d.is_none() {
+ Result::Err(OpenTxError::ItemMissing)
+ } else {
+ let d = Script::from_slice(d.as_slice()).unwrap();
+ Result::Ok(d.calc_script_hash().as_slice().to_vec())
+ }
+ }
+ OpenTxSource::Outpout => {
+ let output = self
+ .transaction
+ .output(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ let d = output.type_().to_opt().ok_or(OpenTxError::ItemMissing)?;
+
+ Result::Ok(d.calc_script_hash().as_slice().to_vec())
+ }
+ OpenTxSource::CellDep => {
+ let outpoint = self.transaction.cell_deps().get(index);
+ if outpoint.is_none() {
+ return Result::Err(OpenTxError::OutOfBound);
+ }
+ let cell = self.provider.get_cell(&outpoint.unwrap().out_point())?;
+ let d = cell.type_().to_opt().ok_or(OpenTxError::ItemMissing)?;
+
+ Result::Ok(d.calc_script_hash().as_slice().to_vec())
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ fn load_field_occupied_capacity(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ ) -> Result, OpenTxError> {
+ match source {
+ OpenTxSource::Input => {
+ let input = self.input_cell(index)?;
+ let data = self.input_cell_data(index)?;
+ Result::Ok(
+ input
+ .occupied_capacity(Capacity::bytes(data.len()).unwrap())
+ .unwrap()
+ .as_u64()
+ .to_le_bytes()
+ .to_vec(),
+ )
+ }
+ OpenTxSource::Outpout => {
+ let output = self.output_cell(index)?;
+ let output_data = self
+ .transaction
+ .outputs_data()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+
+ Result::Ok(
+ output
+ .occupied_capacity(Capacity::bytes(output_data.len()).unwrap())
+ .unwrap()
+ .as_u64()
+ .to_le_bytes()
+ .to_vec(),
+ )
+ }
+ OpenTxSource::CellDep => {
+ let cell = self
+ .transaction
+ .cell_deps()
+ .get(index)
+ .ok_or(OpenTxError::OutOfBound)?;
+ let cell_output = self.provider.get_cell(&cell.out_point())?;
+ let cell_data = self.provider.get_cell_data(&cell.out_point())?;
+ Result::Ok(
+ cell_output
+ .occupied_capacity(Capacity::bytes(cell_data.len()).unwrap())
+ .unwrap()
+ .as_u64()
+ .to_le_bytes()
+ .to_vec(),
+ )
+ }
+ _ => Err(OpenTxError::UnsupportSource),
+ }
+ }
+
+ pub fn load_cell_field(
+ &self,
+ index: usize,
+ source: OpenTxSource,
+ field: OpenTxCellField,
+ ) -> Result, OpenTxError> {
+ match field {
+ OpenTxCellField::Capacity => self.load_field_capacity(index, source),
+ OpenTxCellField::DataHash => self.load_field_data_hash(index, source),
+ OpenTxCellField::Lock => self.load_field_lock(index, source),
+ OpenTxCellField::LockHash => self.load_field_lock_hash(index, source),
+ OpenTxCellField::Type => self.load_field_type(index, source),
+ OpenTxCellField::TypeHash => self.load_field_type_hash(index, source),
+ OpenTxCellField::OccupiedCapacity => self.load_field_occupied_capacity(index, source),
+ }
+ }
+
+ pub fn load_input_field_out_point(&self, index: usize) -> Result, OpenTxError> {
+ Ok(self.input(index)?.previous_output().as_slice().to_vec())
+ }
+
+ pub fn load_input_field_since(&self, index: usize) -> Result, OpenTxError> {
+ Ok(self.input(index)?.since().as_slice().to_vec())
+ }
+
+ pub fn get_cell(&self, index: usize, is_input: bool) -> Result {
+ let cell = if is_input {
+ self.input_cell(index)?
+ } else {
+ self.output_cell(index)?
+ };
+ Ok(cell)
+ }
+}
diff --git a/src/unlock/signer.rs b/src/unlock/signer.rs
index a7da222e..b5a8c66d 100644
--- a/src/unlock/signer.rs
+++ b/src/unlock/signer.rs
@@ -13,8 +13,11 @@ use ckb_types::{
use serde::{Deserialize, Serialize};
use thiserror::Error;
-use crate::types::{AddressPayload, CodeHashIndex, ScriptGroup, Since};
use crate::{constants::MULTISIG_TYPE_HASH, types::omni_lock::OmniLockWitnessLock};
+use crate::{
+ traits::TransactionDependencyProvider,
+ types::{AddressPayload, CodeHashIndex, ScriptGroup, Since},
+};
use crate::{
traits::{Signer, SignerError},
util::convert_keccak256_hash,
@@ -22,6 +25,7 @@ use crate::{
use super::{
omni_lock::{ConfigError, Identity},
+ opentx::{reader::OpenTxReader, OpenTxError},
IdentityFlag, OmniLockConfig,
};
@@ -48,6 +52,8 @@ pub enum ScriptSignError {
#[error("there is an configuration error: `{0}`")]
InvalidConfig(#[from] ConfigError),
+ #[error("open transaction read error: `{0}`")]
+ OpenTxError(#[from] OpenTxError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@@ -64,6 +70,7 @@ pub trait ScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result;
}
@@ -128,6 +135,7 @@ impl ScriptSigner for SecpSighashScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ _tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let args = script_group.script.args().raw_data();
self.sign_tx_with_owner_id(args.as_ref(), tx, script_group)
@@ -273,6 +281,7 @@ impl ScriptSigner for SecpMultisigScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ _tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let witness_idx = script_group.input_indices[0];
let mut witnesses: Vec = tx.witnesses().into_iter().collect();
@@ -365,6 +374,7 @@ impl ScriptSigner for AcpScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ _tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let args = script_group.script.args().raw_data();
let id = &args[0..20];
@@ -414,6 +424,7 @@ impl ScriptSigner for ChequeScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ _tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let args = script_group.script.args().raw_data();
let id = self.owner_id(args.as_ref());
@@ -537,6 +548,7 @@ impl OmniLockScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let witness_idx = script_group.input_indices[0];
let mut witnesses: Vec = tx.witnesses().into_iter().collect();
@@ -550,7 +562,21 @@ impl OmniLockScriptSigner {
let zero_lock = self.config.zero_lock(self.unlock_mode)?;
let zero_lock_len = zero_lock.len();
- let message = generate_message(&tx_new, script_group, zero_lock)?;
+ let (message, open_sig_data) = if self.config.is_opentx_mode() {
+ if let Some(opentx_wit) = self.config.get_opentx_input() {
+ let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?;
+ opentx_wit.generate_message(&reader)?
+ } else {
+ return Err(ScriptSignError::OpenTxError(
+ OpenTxError::InputListConfigMissing,
+ ));
+ }
+ } else {
+ (
+ generate_message(&tx_new, script_group, zero_lock)?,
+ Bytes::new(),
+ )
+ };
let multisig_config = match self.unlock_mode {
OmniUnlockMode::Admin => self
@@ -589,23 +615,29 @@ impl OmniLockScriptSigner {
OmniLockWitnessLock::default()
};
let config_data = multisig_config.to_witness_data();
+ let osdl = open_sig_data.len();
let mut omni_sig = omnilock_witnesslock
.signature()
.to_opt()
.map(|data| data.raw_data().as_ref().to_vec())
.unwrap_or_else(|| {
let mut omni_sig =
- vec![0u8; config_data.len() + multisig_config.threshold() as usize * 65];
- omni_sig[..config_data.len()].copy_from_slice(&config_data);
+ vec![0u8; osdl + config_data.len() + multisig_config.threshold() as usize * 65];
+ if osdl > 0 {
+ omni_sig[..osdl].copy_from_slice(&open_sig_data);
+ }
+ omni_sig[osdl..config_data.len()].copy_from_slice(&config_data);
omni_sig
});
for signature in signatures {
- let mut idx = config_data.len();
+ // every signature should start from begin in case one already exist.
+ let mut idx = osdl + config_data.len();
while idx < omni_sig.len() {
- // Put signature into an empty place.
+ // signautrue already exist
if omni_sig[idx..idx + 65] == signature {
break;
} else if omni_sig[idx..idx + 65] == [0u8; 65] {
+ // Put signature into an empty place.
omni_sig[idx..idx + 65].copy_from_slice(signature.as_ref());
break;
}
@@ -631,6 +663,7 @@ impl OmniLockScriptSigner {
tx: &TransactionView,
script_group: &ScriptGroup,
id: &Identity,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let witness_idx = script_group.input_indices[0];
let mut witnesses: Vec = tx.witnesses().into_iter().collect();
@@ -643,12 +676,20 @@ impl OmniLockScriptSigner {
.build();
let zero_lock = self.config.zero_lock(self.unlock_mode())?;
- let message = generate_message(&tx_new, script_group, zero_lock)?;
+ let (message, open_sig_data) = if let Some(opentx_wit) = self.config.get_opentx_input() {
+ let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?;
+ opentx_wit.generate_message(&reader)?
+ } else {
+ (
+ generate_message(&tx_new, script_group, zero_lock)?,
+ Bytes::new(),
+ )
+ };
let message = convert_keccak256_hash(message.as_ref());
- let signature = self
- .signer
- .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?;
+ let mut signature =
+ self.signer
+ .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?;
// Put signature into witness
let witness_data = witnesses[witness_idx].raw_data();
@@ -657,7 +698,15 @@ impl OmniLockScriptSigner {
} else {
WitnessArgs::from_slice(witness_data.as_ref())?
};
-
+ if self.config.is_opentx_mode() {
+ if let Some(opentx_wit) = self.config.get_opentx_input() {
+ signature = opentx_wit.build_opentx_sig(open_sig_data, signature);
+ } else {
+ return Err(ScriptSignError::OpenTxError(
+ OpenTxError::InputListConfigMissing,
+ ));
+ }
+ }
let lock = Self::build_witness_lock(current_witness.lock(), signature)?;
current_witness = current_witness.as_builder().lock(Some(lock).pack()).build();
witnesses[witness_idx] = current_witness.as_bytes().pack();
@@ -690,6 +739,10 @@ impl ScriptSigner for OmniLockScriptSigner {
return false;
}
+ if args != self.config.build_args() {
+ return false;
+ }
+
if self.unlock_mode == OmniUnlockMode::Admin {
if let Some(admin_config) = self.config.get_admin_config() {
if args.len() < 54 {
@@ -742,6 +795,7 @@ impl ScriptSigner for OmniLockScriptSigner {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
let id = match self.unlock_mode {
OmniUnlockMode::Admin => self
@@ -765,9 +819,18 @@ impl ScriptSigner for OmniLockScriptSigner {
.build();
let zero_lock = self.config.zero_lock(self.unlock_mode)?;
- let message = generate_message(&tx_new, script_group, zero_lock)?;
-
- let signature =
+ let (message, open_sig_data) =
+ if let Some(opentx_wit) = self.config.get_opentx_input() {
+ let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?;
+ opentx_wit.generate_message(&reader)?
+ } else {
+ (
+ generate_message(&tx_new, script_group, zero_lock)?,
+ Bytes::new(),
+ )
+ };
+
+ let mut signature =
self.signer
.sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?;
@@ -778,15 +841,23 @@ impl ScriptSigner for OmniLockScriptSigner {
} else {
WitnessArgs::from_slice(witness_data.as_ref())?
};
-
+ if self.config.is_opentx_mode() {
+ if let Some(opentx_wit) = self.config.get_opentx_input() {
+ signature = opentx_wit.build_opentx_sig(open_sig_data, signature);
+ } else {
+ return Err(ScriptSignError::OpenTxError(
+ OpenTxError::InputListConfigMissing,
+ ));
+ }
+ }
let lock = Self::build_witness_lock(current_witness.lock(), signature)?;
current_witness = current_witness.as_builder().lock(Some(lock).pack()).build();
witnesses[witness_idx] = current_witness.as_bytes().pack();
Ok(tx.as_advanced_builder().set_witnesses(witnesses).build())
}
- IdentityFlag::Ethereum => self.sign_ethereum_tx(tx, script_group, &id),
- IdentityFlag::Multisig => self.sign_multisig_tx(tx, script_group),
+ IdentityFlag::Ethereum => self.sign_ethereum_tx(tx, script_group, &id, tx_dep_provider),
+ IdentityFlag::Multisig => self.sign_multisig_tx(tx, script_group, tx_dep_provider),
IdentityFlag::OwnerLock => {
// should not reach here, just return a clone for compatible reason.
Ok(tx.clone())
diff --git a/src/unlock/unlocker.rs b/src/unlock/unlocker.rs
index eccf417d..911c06ec 100644
--- a/src/unlock/unlocker.rs
+++ b/src/unlock/unlocker.rs
@@ -155,9 +155,9 @@ impl ScriptUnlocker for SecpSighashUnlocker {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
- _tx_dep_provider: &dyn TransactionDependencyProvider,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
- Ok(self.signer.sign_tx(tx, script_group)?)
+ Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?)
}
fn fill_placeholder_witness(
@@ -192,9 +192,9 @@ impl ScriptUnlocker for SecpMultisigUnlocker {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
- _tx_dep_provider: &dyn TransactionDependencyProvider,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
- Ok(self.signer.sign_tx(tx, script_group)?)
+ Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?)
}
fn fill_placeholder_witness(
@@ -429,7 +429,7 @@ impl ScriptUnlocker for AcpUnlocker {
if self.is_unlocked(tx, script_group, tx_dep_provider)? {
self.clear_placeholder_witness(tx, script_group)
} else {
- Ok(self.signer.sign_tx(tx, script_group)?)
+ Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?)
}
}
@@ -575,7 +575,7 @@ impl ScriptUnlocker for ChequeUnlocker {
if self.is_unlocked(tx, script_group, tx_dep_provider)? {
self.clear_placeholder_witness(tx, script_group)
} else {
- Ok(self.signer.sign_tx(tx, script_group)?)
+ Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?)
}
}
@@ -704,9 +704,9 @@ impl ScriptUnlocker for OmniLockUnlocker {
&self,
tx: &TransactionView,
script_group: &ScriptGroup,
- _tx_dep_provider: &dyn TransactionDependencyProvider,
+ tx_dep_provider: &dyn TransactionDependencyProvider,
) -> Result {
- Ok(self.signer.sign_tx(tx, script_group)?)
+ Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?)
}
fn fill_placeholder_witness(