Skip to main content

Transferring fungible assets

Applies to both RGB lines

Both lines follow the same steps, but their invoices, consignments and wallets are incompatible. See Two RGB lines.

An RGB transfer moves state from seals the payer owns to a seal the recipient controls. Only a commitment goes on chain. The data that proves the transfer, the consignment, goes directly from payer to recipient.

  1. Invoice. The recipient creates an invoice naming the contract, the amount and a seal. The seal is either one of the recipient's UTXOs, blinded so the payer can't see which (utxob: in v0.11, at: auth tokens in v0.12), or a new output in the payer's transaction (wvout: / wout:).
  2. Transfer. The payer's wallet selects RGB inputs, builds the state transition, commits it into a PSBT and writes the consignment, all in one step.
  3. Validate. The recipient validates the consignment: the whole history back to genesis, against the schema (v0.11) or codex (v0.12).
  4. Sign and broadcast. Only after the recipient has the consignment does the payer sign and publish the witness transaction.
  5. Accept. Once the transaction confirms, the recipient accepts the consignment into their stash and can spend the new allocation.
Consignment before broadcast

Send the consignment, and wait until the recipient has validated it, before you broadcast. If the witness transaction is published and the consignment is lost, the assets are gone for good. rgb-lib enforces this order: it broadcasts only after the recipient's ACK, unless you set donation to true.

Also:

  • Back up the RGB data, not just the seed. A seed alone doesn't recover assets.
  • Let only the RGB wallet spend its UTXOs. A Bitcoin-only wallet that spends a UTXO holding RGB state destroys it. When a UTXO holds allocations of several contracts, the RGB wallet moves all of them to change. Other wallets don't.

Amounts are not hidden. Every recipient who validates a consignment sees the amounts in the history it contains. What RGB keeps private is everything not in your consignment, and the link between blinded seals and UTXOs.

v0.11​

With the CLI​

The Quick Start, steps 6–9, runs the whole flow on regtest. The commands are:

# recipient
rgb -d data1 -w rcpt1 --electrum=localhost:50001 invoice --amount 100 "$contract_id"
# payer: writes the consignment and a PSBT that already commits to it
rgb -d data0 -w issuer --electrum=localhost:50001 transfer "$invoice" consignment.rgb tx.psbt
# recipient, before anything is broadcast
rgb -d data1 -w rcpt1 --electrum=localhost:50001 validate consignment.rgb
# payer signs with bp-hot (rgb doesn't sign), then finalizes and publishes
bp-hot sign -N tx.psbt wallets/0.derive
rgb -d data0 -w issuer --electrum=localhost:50001 finalize -p tx.psbt tx.tx
# recipient, after confirmation
rgb -d data1 -w rcpt1 --electrum=localhost:50001 accept consignment.rgb

An invoice looks like rgb:<contract>/~/BF/bcrt:utxob:<blinded seal>. Use --address-based for a wvout: invoice, and use long option names: in 0.11.1 the short flag -a belongs to three options of invoice. The fee for transfer is in sats (--fee, default 400). Details are in the CLI reference (v0.11).

With rgb-lib (Rust)​

rgb-lib does the whole exchange through an RGB proxy server: the payer posts the consignment, the recipient fetches it, validates it and posts an ACK, and the payer then broadcasts. Both sides call refresh to move the transfer forward.

This code is for rgb-lib 0.3.0-beta.7 (src/wallet/singlesig.rs, src/wallet/online.rs). It assumes two wallets set up and online as in Issuing fungible tokens. It wasn't run for this page.

use std::collections::HashMap;
use rgb_lib::wallet::{Invoice, Recipient, RgbWalletOpsOnline};
use rgb_lib::Assignment;

let proxy = "rpcs://proxy.iriswallet.com/0.2/json-rpc".to_string();
let expiry = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs()) + 3600;

// Recipient: blind one of its UTXOs and make an invoice.
let receive = bob.blind_receive(
Some(asset_id.clone()),
Assignment::Fungible(1_000),
expiry,
vec![proxy.clone()],
1, // min confirmations
)?;
// Send receive.invoice to the payer.

// Payer: decode the invoice and send (fee rate in sat/vB).
let data = Invoice::new(receive.invoice)?.invoice_data();
let recipient = Recipient {
recipient_id: data.recipient_id,
witness_data: None, // Some(WitnessData { amount_sat, blinding }) for witness_receive invoices
assignment: Assignment::Fungible(1_000),
transport_endpoints: data.transport_endpoints,
};
let result = alice.send(
alice_online,
HashMap::from([(asset_id.clone(), vec![recipient])]),
false, // donation: false = wait for the recipient's ACK before broadcasting
2, // fee rate
1, // min confirmations
expiry,
)?;

// Both sides: refresh until the transfer is Settled.
bob.refresh(bob_online, None, vec![], false)?; // fetch, validate, ACK
alice.refresh(alice_online, None, vec![], false)?; // see the ACK, broadcast
  • witness_receive makes a wvout: invoice instead. The payer then creates the output, paying amount_sat to the recipient.
  • For an external signer, use send_begin (it returns the PSBT), sign it, then call send_end.
  • Pass an empty transport_endpoints list for out-of-band exchange, and move the files yourself with provide_out_of_band_consignment and provide_out_of_band_ack.
  • One send can pay several recipients and several assets in one Bitcoin transaction, with one consignment per asset.

Next​