# Simple CKB Wallet Protocol
## Motivation
As a layer 1 with Turing complete smart contract capability, there are several dApps running on CKB. However, due to CKB's crypto abstraction, the signature verifucation runs at the contract layer rather than the consensus layer, this gives the wallet great flexibility.
There are already several wallets in the CKB ecosystem, such as the MetaMask-based [PW Wallet][pw], the DKIM-based [UniPass Wallet][unipass], the WebAuthn-based [JoyID][joyid], and the most commonly used BIP44-based wallets like [CKB-CLI][ckb-cli] and [Neuron][neuron]. Although these wallets have the ability to sign transactions, some even expose the capability externally, the lack of a unified pattern or standard results in almost every dApp having its own implementation for communication with wallets.
The Web3 ecosystem already has several excellent and widely applicable proposals for wallet communication. This document will reference these proposals and establish patterns specific to CKB, aiming to simplify and unify communication between dApps and wallets.
## Workflow
> `alt` - the corner mark to indicate "if(x) else if(y) ..." in the sequence diagram below
```mermaid
sequenceDiagram
Actor user as User
participant wallet as Wallet
participant dapp as DApp
Note over user,dapp: Establishing Connection
user ->> dapp: access
activate dapp
alt wallet/dapp on the same device
dapp ->> wallet: connect_request<br />(methods,events)
else wallet/dapp on the diff device
dapp ->> user: QR code/deep link
Note left of wallet: scan or paste
user ->> wallet: QR code/deep link
end
alt first pairing
wallet ->> user: permission_request<br />(methods,events)
user ->> wallet: ok
end
wallet ->> dapp: user_identity<br />(address or something else)
deactivate dapp
Note over user,dapp: dApp Business Processing
loop jsonrpc
dapp ->> wallet: jsonrpc_request
activate dapp
alt approval needed
wallet ->> user: method,params
user ->> wallet: ok
end
wallet ->> dapp: jsonrpc_response
deactivate dapp
end
```
## Establishing Connection via WalletConnect
[WalletConnect][walletconnect] is a protocol for connecting wallets to dApps. It is well-document and widely adopted in the Web3 ecosystem. It usually used when the wallet and the dApp are on different devices.
### Session Namespaces
To establish a connection via WalletConnect, the [`Session Namespaces`](https://docs.walletconnect.com/2.0/specs/clients/sign/namespaces) SHOULD be defined as follows:
```json5
{
requiredNamespaces: {
ckb: {
// the `chains` is required by WalletConnect
chains: ["ckb:mainnet"], // or "ckb:testnet"
methods: ["ckb_getAddresses", "ckb_signTransaction", "..."],
events: [],
},
},
}
```
And the received `Session Namespaces`:
```json5
{
sessionNamespaces: {
ckb: {
accounts: ["ckb:mainnet:user_identity"],
methods: ["ckb_getAddresses", "ckb_signTransaction", "..."],
events: [],
},
},
}
```
The `user_identity` in the `accounts` MAY be an address or something that can be used to identify the user. We cannot just use the address as the `user_identity` because of addresses in a BIP44 wallet are derived from a seed, and addresses are always appending, so the `user_identity` depends on the wallet implementation.
## Establishing Connection via Injected Provider
> TODO, this is not in a high priority
## Data Structure and Methods
### `Address`
`Address` is a string in [full payload format address][full-payload-format-address]
```typescript
type Address = string;
```
An address includes information about
- the network type
- what lock script is used
- how to verify the signature
- public key or hash of public key
### `ckb_getAddresses`
CKB uses UTxO model, so a CKB wallet MAY have multiple addresses, even for a transaction, the inputs could be from different addresses. To get the addresses which are actually needed for building a transaction, the dApp may need to filter the addresses
```typescript
type ckb_getAddresses = (options?: object) => Promise<Address[]>;
```
The `ckb_getAddresses` method is designed to accept an `options` parameter, which is an object. The shape of the `options` object is dependent on the wallet implementation, and the wallet SHOULD document the options it supports.
### `TransactionTemplate`
An `Address` in `inputs` that needs to be unlocked by a signature, the signature is usually placed in the `witnesses` field of the transaction. To execute the script in the transaction, the `cellDeps` are also needed.
```typescript
type TransactionTemplate = {
cellDeps: CellDep[];
witnessLock: HexString;
};
```
- `cellDeps` is an array of `CellDep`, which MAY be the code cell of the lock script, or the data cell for providing data for the lock script, e.g. the secp256k1 precomputed table, or a merkle root storage cell.
- `witnessLock` is the witness for unlocking the lock script, it MAY be a signature placeholder, or a merkle proof.
### `ckb_getTransactionTemplate`
The `witnesses` in a transaction is not just for signatures, it may also contain other data, such as the merkle proof when unlocking special lock scripts, such as the [Taproot][taproot-script-witness] and the [UniPass](https://github.com/UniPassID/up-ckb/blob/23b42cbcd63a29ecd4ca6f2d871c2c77baeeb978/src/js-scripts/up-lock-witness.js#LL157C26-L157C26).
```typescript
type ckb_getTransactionTemplate = (
address: Address,
options?: object
) => Promise<TransactionTemplate>;
```
DApp COULD call the `ckb_getTransactionTemplate` method when it needs to build a transaction, once the `TransactionTemplate` is received, the dApp COULD append the `TransactionTemplate` to the transaction before signing it.
### `ckb_signTransaction`
```typescript
type ckb_signTransaction = (
transaction: Transaction,
options?: object
) => Promise<Transaction>;
```
The `ckb_signTransaction` method is designed to sign a transaction, the `options` parameter is optional, and the shape of the `options` object is dependent on the wallet implementation, and the wallet SHOULD document the options it supports.
### `ckb_combineSignTransaction`
The `*sig_all` lock is good for security because of the simple rule which is just verifying the signature, therefore, there are many scripts based on the `*sig_all` lock, such as the [Omnilock][omnilock], [Cheque Lock][cheque-lock], and a new lock called Combine Lock which is just in the design phase. These scripts are act as a proxy to the `*sig_all` lock and append some extra rules for unlocking the lock script.
To make the wallet support signing transactions with these kinds of scripts, dApp could let the wallet know the signing positions, and the wallet SHOULD treat the `SigningPosition` as the `*sig_all` lock, and follow the signing process to sign the transaction.
```typescript
type SigningPosition = [
Address,
number // index of the inputs in the transaction
];
```
Instead of returning a signed transaction, the `ckb_combineSignTransaction` method returns the signature for each signing position. And the dApp SHOULD replace the signature placeholder the actual signature.
```typescript
type Signature = HexString;
```
There MAY be more than one signature are needed in a transaction, so the `ckb_combineSignTransaction` method returns an array of `[Address, Signature]` tuples for each signing position.
```typescript
type ckb_combineSignTransaction = (
transaction: Transaction,
signingPositions: SigningPosition[]
) => Promise<[Address, Signature][]>;
```
### `personal_sign`
Sign a message with a magic prefix [`"Nervos Message:"`](https://github.com/nervosnetwork/neuron/blob/59509826be74f7942c5335f521fa632223c719b6/packages/neuron-wallet/src/services/sign-message.ts#L14). It is helpful for sign in a non-decentralized service.
```typescript
type personal_sign = (
address: Address,
message: HexString | UTF8String
) => Promise<HexString>;
```
The wallet SHOULD display the message to the user, and the user SHOULD approve the message before signing it.
## Example For Adapting Neuron Wallet
### Simple Transfer(Pseudocode)
```typescript
type Secp256k1SigHashAllOptions = {
type: "secp256k1_blake160_sighash_all";
// defaults to false
// indicates whether the address has a transaction history
hasTransactionHistory?: boolean;
// indicates whether the address is change(internal) address
// @see BIP44
change?: "external" | "internal";
};
type Secp256k1MultisigAllOptions = {
type: "secp256k1_blake160_multisig_all";
s?: number;
r?: number;
m?: number;
n?: number;
publicKeyHashes?: Hash[];
};
type impl_ckb_getAddresses = (
options?: Secp256k1SigHashAllOptions | Secp256k1MultisigAllOptions
) => Promise<Address[]>;
```
```typescript
// pairing with Neuron Wallet
walletconnect.pair({
projectId: "...",
requiredNamespaces: {
ckb: {
chains: ["ckb:mainnet"],
methods: [
"ckb_getAddresses",
"ckb_getTransactionTemplate",
"ckb_signTransaction",
],
},
},
});
declare const provider: NeuronProvider;
const usedAddresses = await provider.getAddresses({
hasTransactionHistory: true,
});
const changeAddress = (
await provider.getAddresses({
hasTransactionHistory: false,
})
)[0];
const txTemplate = provider.getTransactionTemplate(usedAddresses[0]);
const liveCells = await provider.collect(_10000ckb, usedAddresses);
await provider.signTransactoin({
cellDeps: txTemplate.cellDeps,
inputs: liveCells,
outputs,
// each different input address should have a signature placeholder in witness.lock
witnesses,
...
});
```
### With Combine Lock(Pseudocode)
```typescript
// will hook the provider
// - getAddresses - get original address from wallet and transform to combine lock address
// - getTransactionTemplate - transform the original transaction template to combine lock transaction template
// - signTransaction - transform the original transaction to combine lock transaction, and call the `ckb_combineSignTransaction` method
type AdaptCombineLock = (provider: Provider) => Provider;
```
## Misc
<details>
<summary>random idea, library, references...</summary>
- [EIP155 Simple replay attack protection](https://eips.ethereum.org/EIPS/eip-155)
```
CHAIN_ID: 1(mainnet)
v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36
```
- [BIP122 URI scheme for Blockchain references](https://github.com/bitcoin/bips/blob/master/bip-0122.mediawiki)
```yaml
blockchain:/block/00000000000000000119af5bcae2926df54ae262e9071a94a99c913cc217cc72
blockchain:/address/16EW6Rv9P9AxFDBrZV816dD4sj1EAYUX3f
```
- [Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md)
```
chain_id: namespace + ":" + reference
namespace: [-a-z0-9]{3,8}
reference: [-_a-zA-Z0-9]{1,32}
```
```yaml
# indicate Bitcoin mainnet
bip122:000000000019d6689c085ae165831e93
```
- Variant BIP44 [CKB-CLI](https://github.com/nervosnetwork/ckb-cli/blob/856875f2bb46afb5db75c7b0fa431e0f10a73f8c/ckb-signer/src/keystore/mod.rs#L35), [Neuron](https://github.com/nervosnetwork/neuron/blob/df6bf9127624474d9146830b7b84eee4304297ee/packages/neuron-wallet/src/models/keys/key.ts#L53)
```
m/44'/309'/0'/change/address_index
```
- [How to sign transaction](https://github.com/nervosnetwork/ckb-system-scripts/wiki/How-to-sign-transaction)
```
digest = hash(
hash(tx) ||
len(witness) ||
witnesses[1:]
)
signature = sign(digest)
```
### Any Solution For More Flexible?
Only communicate with public key, and define a set of [codecs](https://github.com/multiformats/multicodec/blob/master/table.csv) for the wallet to create a digest from the transaction, and sign it.
```
# caculate tx hash
<tx.json>
<blockchain.mol>
CODEC_MOL "RawTransaction"
CODEC_BLAKE2B_256
```
And request signing via JSON-RPC
```json5
{
method: "ckb_sign",
params: [
// a public key to indicate which key to use for signing
"0xpubKey",
// a set of codecs
'<tx.json>\n<blockchain.mol>\nCODEC_MOL "RawTransaction"\nCODEC_BLAKE2B_256',
// transaction
"...",
// blockchain.mol
"...",
],
}
```
The codecs MUST NOT modify the input data, and only used for transform the data, so the wallet can display the raw data to the user for confirmation to ensure that the user is not phished.
### Options is Ambiguous?
- it is not always necessary for all lock scripts
- add some APIs to describe the [options](https://docs.swagger.io/spec.html#5231-object-example), `ckb_getAddressesOptionsDescription`...
</details>
[joyid]: https://docs.joy.id
[ckb-cli]: https://github.com/nervosnetwork/ckb-cli
[neuron]: https://github.com/nervosnetwork/neuron
[pw]: https://github.com/lay2dev/ckb.pw
[unipass]: https://github.com/UniPassID/up-ckb
[walletconnect]: https://docs.walletconnect.com/2.0
[full-payload-format-address]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md#full-payload-format
[taproot-script-witness]: https://blog.cryptape.com/enable-bitcoin-taproot-on-ckb-part-ii#heading-taproot-witness
[omnilock]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md#authentication
[cheque-lock]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0039-cheque/0039-cheque.md