Fiber LogoFiber Docs

Transfer Stablecoins

Learn how to transfer stablecoins between nodes

Requirements
Updated 6/17/2026
latest

TL;DR

Set up three nodes, open UDT (stablecoin) channels, and send a stablecoin payment through a multi-hop path. Builds on the Basic Transfer example.

Overview

This guide demonstrates UDT transfers using stablecoins (RUSD) across 3 nodes via multi-hop routing.

Prerequisites

Setting Up Your Environment

1. Prepare Fiber Binary

Download from GitHub Releases or build from source:

git clone https://github.com/nervosnetwork/fiber.git
cd fiber
cargo build --release

macOS Security

xattr -d com.apple.quarantine fnn fnn-cli

2. Configure Three Nodes

for node in node1 node2 node3; do
  mkdir $node
  cp target/release/fnn $node/
  cp target/release/fnn-cli $node/
  cp config/testnet/config.yml $node/
done

Create a CKB account for each node and export the keys:

ckb-cli account new  # repeat 3 times, save each lock_arg

# For each node directory:
mkdir ckb
ckb-cli account export --lock-arg <node_lock_arg> --extended-privkey-path ./ckb/exported-key
head -n 1 ./ckb/exported-key > ./ckb/key

Key File Format

ckb/key must contain only the 64-character hex private key, no 0x prefix.

Get Testnet funds:

3. Configure Ports

  • Node 1: RPC 8227, P2P 8228
  • Node 2: RPC 8237, P2P 8238
  • Node 3: RPC 8247, P2P 8248
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/16.163.7.105/tcp/8228/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: 10
services:
  - fiber
  - rpc
  - ckb

4. Start All Nodes

# Terminal 1
cd node1 && FIBER_SECRET_KEY_PASSWORD='password1' RUST_LOG=info ./fnn -c config.yml -d .

# Terminal 2
cd node2 && FIBER_SECRET_KEY_PASSWORD='password2' RUST_LOG=info ./fnn -c config.yml -d .

# Terminal 3
cd node3 && FIBER_SECRET_KEY_PASSWORD='password3' RUST_LOG=info ./fnn -c config.yml -d .

Creating Stablecoin Payment Channels

1. Connect Node 1 and Node 2

cd node1 && ./fnn-cli peer connect_peer --address "/ip4/127.0.0.1/tcp/8238/p2p/<node2_peer_id>"
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{
    "id": 42, "jsonrpc": "2.0", "method": "connect_peer",
    "params": [{"pubkey": "<node2_pubkey>", "address": "/ip4/127.0.0.1/tcp/8238"}]
  }'
await sdk.connectPeer({
  address: "/ip4/127.0.0.1/tcp/8238/p2p/<node2_peer_id>",
});

Get Node 2's peer ID and pubkey: cd node2 && ./fnn-cli --url http://127.0.0.1:8237 info

2. Open a Stablecoin Channel (Node 1 → Node 2)

The funding_udt_type_script identifies the RUSD token:

cd node1 && ./fnn-cli channel open_channel \
  --pubkey <node2_pubkey> \
  --funding-amount 10 \
  --public true \
  --funding-udt-type-script '{"code_hash":"0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a","hash_type":"type","args":"0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"}'
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{
    "id": 42, "jsonrpc": "2.0", "method": "open_channel",
    "params": [{
      "pubkey": "<node2_pubkey>",
      "funding_amount": "0xa",
      "public": true,
      "funding_udt_type_script": {
        "code_hash": "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a",
        "hash_type": "type",
        "args": "0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"
      }
    }]
  }'
// funding_amount: 0xa = 10 RUSD
const tempChannelId = await sdk.openChannel({
  pubkey: "<node2_pubkey>",
  fundingAmount: "0xa",
  public: true,
  fundingUdtTypeScript: {
    codeHash: "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a",
    hashType: "type",
    args: "0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b",
  },
});
console.log("Temporary channel ID:", tempChannelId);

CKB Requirement

Opening a UDT channel also requires ~99 CKB in the node's wallet to cover on-chain cell capacity and shutdown fees. Make sure each node has sufficient CKB balance from the faucet.

3. Monitor Channel Status

Wait until state.state_name becomes "ChannelReady":

./fnn-cli channel list_channels
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{"id": 42, "jsonrpc": "2.0", "method": "list_channels", "params": [{}]}'
const channels = await sdk.listChannels();
for (const ch of channels) {
  console.log(`${ch.channelId} — ${ch.state.stateName}`);
}

Repeat steps 1–3 to connect Node 2 and Node 3 (using ports 8247/8248).

Generating Invoices and Making Payments

1. Generate a Stablecoin Invoice on Node 2

cd node2 && ./fnn-cli --url http://127.0.0.1:8237 invoice new_invoice \
  --amount 10 \
  --currency Fibt \
  --description "test stablecoin invoice" \
  --expiry 3600 \
  --udt-type-script '{"code_hash":"0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a","hash_type":"type","args":"0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"}'
curl --location 'http://127.0.0.1:8237' \
  --header 'Content-Type: application/json' \
  --data '{
    "id": 42, "jsonrpc": "2.0", "method": "new_invoice",
    "params": [{
      "amount": "0xa",
      "currency": "Fibt",
      "description": "test stablecoin invoice",
      "expiry": "0xe10",
      "udt_type_script": {
        "code_hash": "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a",
        "hash_type": "type",
        "args": "0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b"
      }
    }]
  }'
// Create invoice on Node 2
const node2 = new FiberSDK({ endpoint: "http://127.0.0.1:8237" });
const { invoiceAddress } = await node2.newInvoice({
  amount: "0xa",
  currency: "Fibt",
  description: "test stablecoin invoice",
  expiry: "0xe10",
  udtTypeScript: {
    codeHash: "0x1142755a044bf2ee358cba9f2da187ce928c91cd4dc8692ded0337efa677d21a",
    hashType: "type",
    args: "0x878fcc6f1f08d48e87bb1c3b3d5083f23f8a39c5d5c764f253b55b998526439b",
  },
});
console.log("Invoice:", invoiceAddress);

The payment_preimage is auto-generated by both CLI and RPC. You can optionally provide your own with "payment_preimage": "<your_preimage>" in the RPC params if needed.

2. Send the Stablecoin Payment from Node 1

cd node1 && ./fnn-cli payment send_payment --invoice "fibt10000000001p..."
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{
    "id": 42, "jsonrpc": "2.0", "method": "send_payment",
    "params": [{"invoice": "fibt10000000001p..."}]
  }'
const result = await sdk.sendPayment({ invoice: "fibt10000000001p..." });
console.log("Payment hash:", result.paymentHash);
console.log("Status:", result.status);

3. Check Channel Balance

# Node 1
./fnn-cli channel list_channels

# Node 2
./fnn-cli --url http://127.0.0.1:8237 channel list_channels
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{"id": 42, "jsonrpc": "2.0", "method": "list_channels", "params": [{}]}'
// Node 1
const node1Channels = await sdk.listChannels();
for (const ch of node1Channels) {
  console.log(`local: ${ch.localBalance}, remote: ${ch.remoteBalance}`);
}

// Node 2
const node2Channels = await node2.listChannels();
for (const ch of node2Channels) {
  console.log(`local: ${ch.localBalance}, remote: ${ch.remoteBalance}`);
}

Closing the Channel

cd node1 && ./fnn-cli channel shutdown_channel \
  --channel-id <channel_id> \
  --close-script '{"code_hash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8","hash_type":"type","args":"<your_lock_arg>"}' \
  --fee-rate 1000
curl --location 'http://127.0.0.1:8227' \
  --header 'Content-Type: application/json' \
  --data '{
    "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": "0x3e8"
    }]
  }'
await sdk.shutdownChannel({
  channelId: "<channel_id>",
  closeScript: {
    codeHash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
    hashType: "type",
    args: "<your_lock_arg>",
  },
  feeRate: "0x3e8",
});
console.log("Channel closed.");

close_script

Get args from fnn-cli info under default_funding_lock_script.args. The fee_rate (shannons/KW) is optional and defaults to 1000. You can also use --force (CLI) or "force": true (RPC) to force-close a channel unilaterally, in which case close_script and fee_rate are ignored.

Important Notes

  • RUSD amounts: CLI uses base units (e.g. 10 = 10 RUSD); RPC uses hex strings (e.g. "0xa" = 10)
  • Invoice currency: Use "Fibt" for testnet, not "RUSD"
  • auto_accept_amount: Channels with funding below this value require manual acceptance
  • Channel state format: The list_channels response returns state as a nested object: state.state_name (e.g. "ChannelReady") and state.state_flags (e.g. ["PublicChannel"])
  • Gossip sync time: After starting nodes, wait for the gossip protocol to fully sync the network graph before attempting multi-hop payments. This may take several minutes depending on network conditions.