Basic Transfer Example
Learn how to set up and execute transfers between two nodes
TL;DR
Set up two local nodes, open a channel between them, and send a CKB payment from one to the other. The whole process takes about 10 minutes.
Overview
This guide walks you through setting up and executing a basic token (CKB) transfer between two nodes on the Fiber Testnet.
Prerequisites
- Git (if building from source)
- Rust and Cargo (if building from source)
- Basic understanding of command line operations
- curl for making RPC calls
- ckb-cli for generating keys
Setting Up Your Nodes
1. Prepare Fiber Binary
Download the latest release binary from the Fiber GitHub Releases page, or build from source:
git clone https://github.com/nervosnetwork/fiber.git
cd fiber
cargo build --releasemacOS Security
If you're using macOS, remove the quarantine attribute:
xattr -d com.apple.quarantine fnn fnn-cliHTTP Proxy Issues
If you encounter 503 errors when using fnn-cli, run:
export NO_PROXY=127.0.0.1,localhost2. Create Data Directories
# For Node 1
mkdir node1
cp target/release/fnn node1/
cp target/release/fnn-cli node1/
cp config/testnet/config.yml node1/
# For Node 2
mkdir node2
cp target/release/fnn node2/
cp target/release/fnn-cli node2/
cp config/testnet/config.yml node2/3. Configure Node Keys
Each node needs its own private key. Create two separate CKB accounts:
ckb-cli account new # for Node 1
ckb-cli account new # for Node 2Export the keys:
# In node1 directory
mkdir ckb
ckb-cli account export --lock-arg <node1_lock_arg> --extended-privkey-path ./ckb/exported-key
head -n 1 ./ckb/exported-key > ./ckb/key
# In node2 directory
mkdir ckb
ckb-cli account export --lock-arg <node2_lock_arg> --extended-privkey-path ./ckb/exported-key
head -n 1 ./ckb/exported-key > ./ckb/keyKey File Format
The ckb/key file must contain only the 64-character hex private key (first line), without 0x prefix.
Get Testnet funds from https://faucet.nervos.org for both nodes.
4. Configure Ports
Edit config.yml for each node:
- Node 1: RPC Port
8227, P2P Port8228 - Node 2: RPC Port
8237, P2P Port8238
View complete config.yml example
fiber:
listening_addr: "/ip4/127.0.0.1/tcp/8228"
bootnode_addrs:
- "/ip4/54.179.226.154/tcp/8228/p2p/Qmes1EBD4yNo9Ywkfe6eRw9tG1nVNGLDmMud1xJMsoYFKy"
- "/ip4/54.179.226.154/tcp/18228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
announce_listening_addr: true
chain: testnet
scripts:
- name: FundingLock
script:
code_hash: 0x6c67887fe201ee0c7853f1682c0b77c0e6214044c156c7558269390a8afa6d7c
hash_type: type
args: 0x
cell_deps:
- type_id:
code_hash: 0x00000000000000000000000000000000000000000000000000545950455f4944
hash_type: type
args: 0x3cb7c0304fe53f75bb5727e2484d0beae4bd99d979813c6fc97c3cca569f10f6
- cell_dep:
out_point:
tx_hash: 0x12c569a258dd9c5bd99f632bb8314b1263b90921ba31496467580d6b79dd14a7
index: 0x0
dep_type: code
- name: CommitmentLock
script:
code_hash: 0x740dee83f87c6f309824d8fd3fbdd3c8380ee6fc9acc90b1a748438afcdf81d8
hash_type: type
args: 0x
cell_deps:
- type_id:
code_hash: 0x00000000000000000000000000000000000000000000000000545950455f4944
hash_type: type
args: 0xf7e458887495cf70dd30d1543cad47dc1dfe9d874177bf19291e4db478d5751b
- cell_dep:
out_point:
tx_hash: 0x12c569a258dd9c5bd99f632bb8314b1263b90921ba31496467580d6b79dd14a7
index: 0x0
dep_type: code
rpc:
listening_addr: "127.0.0.1:8227"
ckb:
rpc_url: "https://testnet.ckbapp.dev/"
udt_whitelist:
- name: RUSD
script:
code_hash: 0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a
hash_type: type
args: 0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b
cell_deps:
- type_id:
code_hash: 0x00000000000000000000000000000000000000000000000000545950455f4944
hash_type: type
args: 0x97d30b723c0b2c66e9cb8d4d0df4ab5d7222cbb00d4a9a2055ce2e5d7f0d8b0f
auto_accept_amount: 1000000000
services:
- fiber
- rpc
- ckbStep-by-Step Transfer Process
1. Start Both Nodes
# Terminal 1 — Node 1
cd node1
FIBER_SECRET_KEY_PASSWORD='password1' RUST_LOG=info ./fnn -c config.yml -d .
# Terminal 2 — Node 2
cd node2
FIBER_SECRET_KEY_PASSWORD='password2' RUST_LOG=info ./fnn -c config.yml -d .2. Connect the Nodes
First get Node 2's pubkey, then connect from Node 1:
# Get Node 2's pubkey
cd node2 && ./fnn-cli --url http://127.0.0.1:8237 info | grep pubkey
# Connect from Node 1
cd node1 && ./fnn-cli peer connect_peer --pubkey <node2_pubkey> --address "/ip4/127.0.0.1/tcp/8238"# Get Node 2's pubkey
curl -s -X POST -H "Content-Type: application/json" \
-d '{"id":"42","jsonrpc":"2.0","method":"node_info"}' \
http://localhost:8237 | grep pubkey
# Connect from Node 1
curl -s -X POST -H "Content-Type: application/json" \
-d '{
"id": "42", "jsonrpc": "2.0", "method": "connect_peer",
"params": [{"pubkey": "<node2_pubkey>", "address": "/ip4/127.0.0.1/tcp/8238"}]
}' http://localhost:82273. Open a Payment Channel
# funding-amount is in shannons (50000000000 = 500 CKB)
cd node1 && ./fnn-cli channel open_channel \
--pubkey <node2_pubkey> \
--funding-amount 50000000000 \
--public true# funding_amount in hex (0xba43b7400 = 500 CKB)
curl -s -X POST -H "Content-Type: application/json" \
-d '{
"id": "42", "jsonrpc": "2.0", "method": "open_channel",
"params": [{"pubkey": "<node2_pubkey>", "funding_amount": "0xba43b7400", "public": true}]
}' http://localhost:8227Check channel status — wait until state_name becomes CHANNEL_READY:
./fnn-cli channel list_channelscurl -s -X POST -H "Content-Type: application/json" \
-d '{"id":"42","jsonrpc":"2.0","method":"list_channels","params":[{}]}' \
http://localhost:82274. Generate an Invoice
Create a payment invoice on Node 2 for 100 CKB:
# amount in shannons (10000000000 = 100 CKB)
cd node2 && ./fnn-cli --url http://127.0.0.1:8237 invoice new_invoice \
--amount 10000000000 \
--currency Fibt \
--description "test invoice"# Generate a random payment_preimage first
payment_preimage="0x$(openssl rand -hex 32)"
curl -s -X POST -H "Content-Type: application/json" \
-d "{
\"id\": \"42\", \"jsonrpc\": \"2.0\", \"method\": \"new_invoice\",
\"params\": [{
\"amount\": \"0x2540be400\",
\"currency\": \"Fibt\",
\"description\": \"test invoice\",
\"expiry\": \"0xe10\",
\"payment_preimage\": \"$payment_preimage\",
\"hash_algorithm\": \"sha256\"
}]
}" http://localhost:82375. Make the Payment
cd node1 && ./fnn-cli payment send_payment --invoice "fibt100000000001p..."curl -s -X POST -H "Content-Type: application/json" \
-d '{
"id": "42", "jsonrpc": "2.0", "method": "send_payment",
"params": [{"invoice": "fibt100000000001p..."}]
}' http://localhost:82276. Verify the Transfer
Check channel balances to confirm the transfer:
# Node 1
./fnn-cli channel list_channels
# Node 2
./fnn-cli --url http://127.0.0.1:8237 channel list_channelscurl -s -X POST -H "Content-Type: application/json" \
-d '{"id":"42","jsonrpc":"2.0","method":"list_channels","params":[{}]}' \
http://localhost:8227After sending 100 CKB from Node 1, Node 1's local_balance decreases by ~100 CKB and remote_balance increases by 100 CKB.
Closing the Channel
cd node1 && ./fnn-cli channel shutdown_channel \
--channel-id <channel_id> \
--force truecurl -s -X POST -H "Content-Type: application/json" \
-d '{
"id": "42", "jsonrpc": "2.0", "method": "shutdown_channel",
"params": [{
"channel_id": "<channel_id>",
"close_script": {
"code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
"hash_type": "type",
"args": "<your_lock_arg>"
},
"fee_rate": "0x3FC"
}]
}' http://localhost:8227CLI vs RPC for closing
With CLI --force true, close_script and fee_rate are determined automatically. With RPC, you must provide close_script matching your node's CKB lock script (get it from ckb-cli account list).
Next Steps
- Transfer Stablecoins — transfer RUSD through a multi-hop path
- Connect Public Nodes — connect to the public Testnet nodes