Skip to content
Merged
5 changes: 5 additions & 0 deletions .docs-plan/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Record decisions that constrain future work — things an agent needs to know th

---

## 2026-04-17: Added chain-fusion-signer, papi, ic-pub-key submodules
**Context:** Page proposal #24 (chain-fusion signer guide) required source material not yet in `.sources/`
**Decision:** Added three new release-pinned submodules: `chain-fusion-signer` (v0.3.0), `papi` (v0.1.1), `ic-pub-key` (v1.0.1)
**Rationale:** All three are needed to write accurate code examples — signer API, ICRC-2 payment setup, and CLI usage respectively. All follow "track latest release" strategy.

## 2026-03-11: Diataxis framework with 5 top-level sections
**Context:** Need a clear information architecture for the new developer docs
**Decision:** Use Diataxis with 5 sections: Getting Started (tutorials), Guides (how-to), Concepts (explanations), Languages (synced + hand-written), Reference (information)
Expand Down
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@
[submodule ".sources/dotskills"]
path = .sources/dotskills
url = git@github.com:vincentkoc/dotskills.git
[submodule ".sources/chain-fusion-signer"]
path = .sources/chain-fusion-signer
url = git@github.com:dfinity/chain-fusion-signer.git
[submodule ".sources/papi"]
path = .sources/papi
url = git@github.com:dfinity/papi.git
[submodule ".sources/ic-pub-key"]
path = .sources/ic-pub-key
url = git@github.com:dfinity/ic-pub-key.git
3 changes: 3 additions & 0 deletions .sources/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
# instead. This is expected behaviour with --depth 1 submodule init.
# -------------------------------------------------------

chain-fusion-signer v0.3.0 8cea727
papi v0.1.1 168bc9d
ic-pub-key v1.0.1 f89fa55
icp-cli v0.2.3 caeac37
motoko v1.5.1 # tag 8e4c02d not in shallow clone 395ce0b
motoko-core v2.4.0 cd37dbf
Expand Down
1 change: 1 addition & 0 deletions .sources/chain-fusion-signer
Submodule chain-fusion-signer added at 8cea72
1 change: 1 addition & 0 deletions .sources/ic-pub-key
Submodule ic-pub-key added at f89fa5
1 change: 1 addition & 0 deletions .sources/papi
Submodule papi added at 168bc9
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,9 @@ For current release hashes, see `.sources/VERSIONS`.
| `.sources/cdk-rs` | `dfinity/cdk-rs` | latest release | Rust CDK (`ic-cdk`, `ic-cdk-timers`, `ic-cdk-macros`) — API signatures, management canister types |
| `.sources/candid` | `dfinity/candid` | latest release | Candid spec, type system, `didc` tool source |
| `.sources/response-verification` | `dfinity/response-verification` | latest release | Response verification, certified variables, certificate trees |
| `.sources/chain-fusion-signer` | `dfinity/chain-fusion-signer` | latest release | Chain Fusion Signer canister — API, key derivation, address generation, ICRC-2 payment model |
| `.sources/papi` | `dfinity/papi` | latest release | PAPI (payment API) — cycle payment interface used by the Chain Fusion Signer |
| `.sources/ic-pub-key` | `dfinity/ic-pub-key` | latest release | `@dfinity/ic-pub-key` CLI tool for deriving public keys and addresses via the Chain Fusion Signer |
| `.sources/dotskills` | `vincentkoc/dotskills` | `main` | Technical documentation skill (AGPL-3.0 — kept as submodule to avoid license mixing) |

### Submodule initialization
Expand Down Expand Up @@ -766,6 +769,9 @@ EOF
| `candid` | Check for spec changes that affect the Candid reference page or type-mapping examples |
| `response-verification` | Check for API changes affecting certified variables patterns in docs |
| `dotskills` | Check if the `technical-documentation` skill changed in ways that affect review criteria or authoring rules |
| `chain-fusion-signer` | Check for changed canister IDs, API methods, or key derivation patterns — grep chain-fusion guide and any pages referencing the signer |
| `papi` | Check for changed payment interface or cycle cost model — update any payment setup examples in the chain-fusion signer guide |
| `ic-pub-key` | Check for changed CLI flags or commands — update CLI examples in the chain-fusion signer guide |

### Synced files from submodules

Expand Down
275 changes: 275 additions & 0 deletions docs/guides/chain-fusion/chain-fusion-signer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
---
title: "Chain Fusion Signer"
description: "Use the Chain Fusion Signer canister to sign transactions for Bitcoin, Ethereum, and other chains from web apps and the command line — no backend canister required."
sidebar:
order: 5
---

The [Chain Fusion Signer](https://github.com/dfinity/chain-fusion-signer) is a public canister on ICP that exposes the IC's threshold signature APIs directly to web apps and CLI users. Normally, accessing threshold ECDSA or Schnorr requires deploying your own backend canister. With the Chain Fusion Signer, you call a shared, governance-controlled canister instead.

**Canister ID (mainnet):** `grghe-syaaa-aaaar-qabyq-cai`

The signer charges callers in cycles for each API call. You pre-approve the signer to withdraw from your cycles ledger account using ICRC-2 before making calls.

## Prerequisites

- An ICP identity with cycles in the [Cycles Ledger](../../reference/system-canisters.md#cycles-ledger) (`um5iw-rqaaa-aaaaq-qaaba-cai`)
- icp-cli installed and authenticated (`icp identity whoami`)
- For offline address derivation: Node.js and `npx`

## Approve payment

Every signer API call deducts cycles from your cycles ledger account. Before calling the signer, approve it to spend cycles on your behalf. One approval covers multiple calls until the allowance is exhausted.

```bash
SIGNER="grghe-syaaa-aaaar-qabyq-cai"
CYCLES_LEDGER="um5iw-rqaaa-aaaaq-qaaba-cai"

# Approve 1 trillion cycles — enough for ~27 signing operations
icp canister call "$CYCLES_LEDGER" icrc2_approve \
"(record {
amount = 1_000_000_000_000 : nat;
spender = record { owner = principal \"${SIGNER}\" };
})" \
--network ic
```

See [API fees](#api-fees) for per-method costs.

## Get your Ethereum address

Each principal has a deterministic Ethereum address on the signer. Retrieve it with:

```bash
icp canister call "$SIGNER" eth_address_of_caller \
'(opt variant { CallerPaysIcrc2Cycles })' \
--network ic
```

```candid
(variant { Ok = record { address = "0xf53e047376e37eAc56d48245B725c47410cf6F1e" } })
```

To look up the address of another principal:

```bash
icp canister call "$SIGNER" eth_address \
"(record { \"principal\" = opt principal \"<TARGET_PRINCIPAL>\" },
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

### Derive offline (no cycles)

Address derivation involves no secret key material, so it can be done offline using `@dfinity/ic-pub-key`:

```bash
npx @dfinity/ic-pub-key signer eth address -u <YOUR_PRINCIPAL>
```

This produces the same address as the onchain call but costs no cycles.

## Sign an Ethereum transaction

Build and sign an EIP-1559 transaction:

```bash
icp canister call "$SIGNER" eth_sign_transaction \
"(record {
to = \"0xRecipientAddress\";
chain_id = 1 : nat;
nonce = 0 : nat;
gas = 21000 : nat;
max_fee_per_gas = 20_000_000_000 : nat;
max_priority_fee_per_gas = 1_000_000_000 : nat;
value = 1_000_000_000_000_000_000 : nat;
data = null;
},
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

To sign a pre-hashed message:

```bash
icp canister call "$SIGNER" eth_sign_prehash \
"(record { hash = \"0x<32-byte-hex-hash>\" },
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

## Get your Bitcoin address

```bash
icp canister call "$SIGNER" btc_caller_address \
"(record { network = variant { mainnet }; address_type = variant { P2WPKH } },
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

### Derive offline (no cycles)

```bash
npx @dfinity/ic-pub-key signer btc address -u <YOUR_PRINCIPAL> -n mainnet
```

For testnet, use `-n testnet`.

## Check your Bitcoin balance

```bash
icp canister call "$SIGNER" btc_caller_balance \
"(record {
network = variant { mainnet };
address_type = variant { P2WPKH };
min_confirmations = null;
},
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

## Send Bitcoin

```bash
icp canister call "$SIGNER" btc_caller_send \
"(record {
network = variant { mainnet };
address_type = variant { P2WPKH };
utxos_to_spend = vec {};
fee_satoshis = null;
outputs = vec {
record {
destination_address = \"bc1qRecipientAddress\";
sent_satoshis = 10000 : nat64;
}
};
},
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

`utxos_to_spend` selects specific UTXOs. Pass `vec {}` to let the signer choose automatically.

## Generic ECDSA signing

Use `generic_sign_with_ecdsa` when you need raw threshold ECDSA signatures for chains the signer does not have dedicated methods for:

```bash
icp canister call "$SIGNER" generic_sign_with_ecdsa \
"(opt variant { CallerPaysIcrc2Cycles },
record {
key_id = record { name = \"key_1\"; curve = variant { secp256k1 } };
derivation_path = vec { blob \"my_app\"; blob \"user_key_1\" };
message_hash = blob \"<32-byte-message-hash>\";
})" \
--network ic
```

Use a stable derivation path to get the same key every time. Different paths yield independent keys.

## Schnorr signing

```bash
icp canister call "$SIGNER" schnorr_sign \
"(record {
key_id = record { algorithm = variant { ed25519 }; name = \"key_1\" };
derivation_path = vec { blob \"my_app\"; blob \"user_key_1\" };
message = blob \"<message-bytes>\";
},
opt variant { CallerPaysIcrc2Cycles })" \
--network ic
```

## Web app integration

In a web app, call the signer from the browser using a generated TypeScript actor. Generate bindings from the signer's Candid interface:

```bash
# Download the signer's Candid interface
icp canister metadata "$SIGNER" candid:service --network ic > signer.did

# Generate TypeScript bindings (requires @icp-sdk/bindgen)
npx @icp-sdk/bindgen --did-file signer.did --out-dir src/declarations/signer
```

Then create actors for both the Cycles Ledger (for payment approval) and the signer:

```typescript
import { createActor as createCyclesLedgerActor } from './declarations/cycles_ledger';
import { createActor as createSignerActor } from './declarations/signer';

async function approveAndSign(identity: Identity, messageHash: string) {
const agent = await HttpAgent.create({ identity });

const cyclesLedger = createCyclesLedgerActor('um5iw-rqaaa-aaaaq-qaaba-cai', { agent });
const signer = createSignerActor('grghe-syaaa-aaaar-qabyq-cai', { agent });

// Pre-approve 1 trillion cycles
await cyclesLedger.icrc2_approve({
amount: 1_000_000_000_000n,
spender: { owner: Principal.fromText('grghe-syaaa-aaaar-qabyq-cai'), subaccount: [] },
expires_at: [],
fee: [],
memo: [],
from_subaccount: [],
created_at_time: [],
expected_allowance: [],
});

// Sign the prehash
const result = await signer.eth_sign_prehash(
{ hash: messageHash },
[{ CallerPaysIcrc2Cycles: null }]
);

if ('Err' in result) throw new Error(JSON.stringify(result.Err));
return result.Ok.signature;
}
```

<!-- Needs human verification: TypeScript actor creation pattern — verify against generated bindings for the actual signer IDL -->

OISY Wallet uses the Chain Fusion Signer as its production signing backend and serves as a reference implementation. OISY uses `PatronPaysIcrc2Cycles` — the OISY backend canister pre-approves cycles on each user's behalf, so individual users pay no cycles directly.

## API fees

Fees are charged per call in cycles. Verify against the [source](https://github.com/dfinity/chain-fusion-signer/blob/main/src/signer/api/src/methods.rs) for the latest values (table reflects v0.3.0):

| Method | Fee (cycles) |
|--------|-------------|
| `eth_address`, `eth_address_of_caller` | 77,000,000 |
| `btc_caller_address` | 79,000,000 |
| `generic_caller_ecdsa_public_key`, `schnorr_public_key` | 77,000,000 |
| `btc_caller_balance` | 113,000,000 |
| `eth_personal_sign`, `eth_sign_prehash`, `eth_sign_transaction` | 37,000,000,000 |
| `generic_sign_with_ecdsa`, `schnorr_sign` | 37,000,000,000 |
| `btc_caller_send`, `btc_caller_sign` | 132,000,000,000 |

Fees are set at approximately 140% of the typical call cost to cover failed-call overhead.

## Payment variants

The `opt PaymentType` argument accepts these variants:

| Variant | Description |
|---------|-------------|
| `CallerPaysIcrc2Cycles` | Caller pre-approves the Cycles Ledger; recommended for CLI and web apps |
| `CallerPaysIcrc2Tokens { ledger }` | Pay via ICRC-2 token transfer; for this canister the ledger is hardcoded to the Cycles Ledger |
| `PatronPaysIcrc2Cycles { owner; subaccount }` | A patron account covers costs on the caller's behalf |
| `PatronPaysIcrc2Tokens { owner; subaccount }` | Patron pays via ICRC-2 token; for this canister the ledger is hardcoded to the Cycles Ledger |
| `AttachedCycles` | Cycles attached directly to the call (requires proxy canister support) |

Pass `null` instead of a payment type to use the canister's default, which is `CallerPaysIcrc2Cycles`.

**Note on token variants:** `CallerPaysIcrc2Tokens` and `PatronPaysIcrc2Tokens` are supported by this canister, but the ledger is hardcoded to the Cycles Ledger — they do not accept arbitrary tokens such as ckBTC or ckETH. All five variants settle in cycles.

These variants are defined by [papi](https://github.com/dfinity/papi), an open-source Rust library for adding payment gateways to ICP canisters. The Chain Fusion Signer uses papi internally to handle fee collection. If you want to charge callers in your own canister — using the same `CallerPaysIcrc2Cycles` or `PatronPaysIcrc2Cycles` patterns — papi provides the implementation.

## Next steps

- [Bitcoin integration guide](bitcoin.md) — build a full Bitcoin dapp with your own signing backend
- [Ethereum integration guide](ethereum.md) — EVM RPC canister for reading Ethereum state
- [Cycles Ledger](../../reference/system-canisters.md#cycles-ledger) — fund your account with cycles
- [papi](https://github.com/dfinity/papi) — add the same `CallerPaysIcrc2Cycles` / `PatronPaysIcrc2Cycles` payment pattern to your own canister

<!-- Upstream: informed by dfinity/chain-fusion-signer — src/signer/canister/signer.did, src/signer/api/src/methods.rs, README.md, check-pricing.report.md; dfinity/papi — README.md (payment variants and patron pattern); dfinity/ic-pub-key — README.md, src/cli.ts -->
Loading