# Basic architecture
ckb-cli communicate with plugins by starting a plugin process and read/write request/response tough stdin/stdout. So it should be possible to write them in any language, and a crashing plugin should not cause ckb-cli crash.
There are 4 role types.
```rust
pub enum PluginRole {
// The argument is for if keystore need password
KeyStore { require_password: bool },
Indexer,
// The argument is for where the sub-command is injected to.
SubCommand { name: String },
// The argument is for the callback function name
Callback { name: CallbackName },
}
```
The `key_store` and `indexer` role plugin can replace the default implementation and can be accessed by all plugins by sending request to stdout and then receive response from stdin.
The `sub_command` role plugin will add a top level sub-command in ckb-cli, the plugin will need to parse the command line argument itself.
The `callback` role plugin will be called when certain event happend (send transaction for example).
Here is the config return as the response of `get_config` method.
```rust
struct PluginConfig {
name: String,
description: String,
daemon: bool,
roles: Vec<PluginRole>,
}
```
One plugin can have multiple roles.
A plugin can define as `daemon` pluign, ckb-cli will start all actived `daemon` plugin processes when ckb-cli start and let them keep running. ckb-cli will start a non-daemon when needed, send request to its stdin and wait the response from its stdout then kill the process.
The plugin can access rpc request by `rpc_` prefixed methods, they are just proxies of [CKB json-rpc](https://github.com/nervosnetwork/ckb/blob/develop/rpc/README.md) calls. It is useful when implement your own indexer.
# RPC protocol
The rpc is follow jsonrpc 2.0 protocol. For rust user, `plugin-protocl` package provide a more semantic interface.
## Get config of the plugin
#### Request
```javascript
{
"params": [],
"method": "get_config",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "plugin_config",
"content": {
"roles": [
{
"role": "key_store",
"require_password": true
}
],
"name": "demo_keystore",
"description": "It's a keystore for demo",
"daemon": true
}
},
"id": 0,
"jsonrpc": "2.0"
}
```
## keystore methods
### List accounts
#### Request
```javascript
{
"params": [],
"method": "keystore_list_account",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "h160_vec",
"content": [
"0xe22f7f385830a75e50ab7fc5fd4c35b134f1e84b",
"0x13e41d6f9292555916f17b4882a5477c01270142",
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
]
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Create an account
#### Request
```javascript
{
"params": [
// (optional) The password to encrypt the account
"123"
],
"method": "keystore_create_account",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "h160",
"content": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Update password
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64",
// The password to decrypt the account
"123",
// The password to encrypt the account
"123"
],
"method": "keystore_update_password",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "ok"
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Import an account
#### Request
```javascript
{
"params": [
// The secp256k1 key of master private key
"0x0303030303030303030303030303030303030303030303030303030303030303",
// The chain code of master private key
"0x0404040404040404040404040404040404040404040404040404040404040404",
// (optional) The password to encrypt the account
"123"
],
"method": "keystore_import",
"id": 0,
"jsonrpc": "2.0"
}
```
### Export an account
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64",
// (optional) The password to encrypt the account
"123"
],
"method": "keystore_export",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "master_private_key",
"content": {
"privkey": "0x0303030303030303030303030303030303030303030303030303030303030303",
"chain_code": "0x0404040404040404040404040404040404040404040404040404040404040404"
}
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Sign a message
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64",
// A derivation key path ("m" for master key)
"m/44'/309'/0'/0/19",
// The message to sign (H256)
"0xe203d8260a0eb9d0ec8f69976e2108d9e50d0c8fb1920a67d10d61cb9993e284",
// The sign target, a transaction or any message
{
"type": "transaction",
"content": {
"version": "0x0",
"cell_deps": [
{
"out_point": {
"tx_hash": "0xd6ae21528966b5926a95b5dfa75281e91f071af492ba7879aff29d671c7bb523",
"index": "0x0"
},
"dep_type": "dep_group"
}
],
"header_deps": [],
"inputs": [
{
"since": "0x0",
"previous_output": {
"tx_hash": "0xb79cc8daf20601d5cefda345951e21390fc5e2c6dab33c7a39207f64fb947731",
"index": "0x7"
}
}
],
"outputs": [
{
"capacity": "0x174876e800",
"lock": {
"code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
"hash_type": "type",
"args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
},
"type": null
},
{
"capacity": "0x1bc16d5005b88180",
"lock": {
"code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
"hash_type": "type",
"args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7"
},
"type": null
}
],
"outputs_data": [
"0x",
"0x"
],
"witnesses": []
}
},
// Sign use recoverable signature
true,
// (optional) The password to decrypt the account
"123"
],
"method": "keystore_sign",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "bytes",
"content": "0x0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Get the extended public key of an account
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64",
// A derivation key path ("m" for master key)
"m/44'/309'/0'/0/19",
// (optional) The password to decrypt the account
"123"
],
"method": "keystore_extended_pubkey",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "bytes",
"content": "0x02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Get derived key set (for HD wallet cell collection)
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xe8b7cfc565396a49efe154e81fe02c2bca9f3621",
// Maximum external keys to search
100,
// The last change address been used (for know the next change address)
"0xe8b7cfc565396a49efe154e81fe02c2bca9f3621",
// Maximum change keys to search
10000,
// (optional) The password to decrypt the account
"123"
],
"method": "keystore_derived_key_set",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "derived_key_set",
"content": {
"external": [
[
"m/44'/309'/0'/0/19",
"0x13e41d6f9292555916f17b4882a5477c01270142"
],
[
"m/44'/309'/0'/0/20",
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
]
],
"change": [
[
"m/44'/309'/0'/1/19",
"0x13e41d6f9292555916f17b4882a5477c01270142"
],
[
"m/44'/309'/0'/1/20",
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
]
]
}
},
"id": 0,
"jsonrpc": "2.0"
}
```
### Get derived key set by path index
#### Request
```javascript
{
"params": [
// The blake160 hash of the public key (account identifier)
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64",
// The external path start index, include the starting index (template: "m/44'/309'/0'/0/{index}")
0,
// The length of external derived key
20,
// The change path start index, include the starting index (template: "m/44'/309'/0'/1/{index}")
0,
// The length of change derived key
10,
// (optional) The password to decrypt the account
"123"
],
"method": "keystore_derived_key_set_by_index",
"id": 0,
"jsonrpc": "2.0"
}
```
#### Response
```javascript
{
"result": {
"type": "derived_key_set",
"content": {
"external": [
[
"m/44'/309'/0'/0/19",
"0x13e41d6f9292555916f17b4882a5477c01270142"
],
[
"m/44'/309'/0'/0/20",
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
]
],
"change": [
[
"m/44'/309'/0'/1/19",
"0x13e41d6f9292555916f17b4882a5477c01270142"
],
[
"m/44'/309'/0'/1/20",
"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
]
]
}
},
"id": 0,
"jsonrpc": "2.0"
}
```
# A keystore demo plugin
First you need build the ckb-cli and the example plugin:
```shell
# Build ckb-cli
cargo build
# Build example keystore plugin
cd plugin-protocol
cargo build --examples
```
Then, start ckb-cli and install the plugin:
```shell
./target/debug/ckb-cli
CKB> plugin install --binary-path ./target/debug/examples/keystore
daemon: true
description: "It's a keystore for demo"
name: demo_keystore
```
If you want see all the debug log messages from plugin module, you can start ckb-cli by:
```shell
RUST_LOG=ckb_cli::plugin=debug ./target/debug/ckb-cli
```
Show the detail infromation of the plugin:
``` shell
CKB> plugin info --name demo_keystore
daemon: true
description: "It's a keystore for demo"
is_active: true
name: demo_keystore
roles:
- require_password: true
role: key_store
```
Sign a message by recoverable signature to test the plugin:
``` shell
CKB> util sign-message --from-account 0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64 --message 0xe203d8260a0eb9d0ec8f69976e2108d9e50d0c8fb1920a67d10d61cb9993e284 --recoverable
Password: ***
recoverable: true
signature: 0x0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101
```
If you don't want the plugin, just deactive it.
``` shell
CKB> plugin deactive --name demo_keystore
Plugin demo_keystore is deactived!
CKB> util sign-message --from-account 0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64 --message 0xe203d8260a0eb9d0ec8f69976e2108d9e50d0c8fb1920a67d10d61cb9993e284 --recoverable
Password: ***
Account not found: b39bbc0b3673c7d36450bc14cfcdad2d559c6c64
```