r14 transfer
Execute a private transfer with a ZK proof.
Usage
r14 transfer <value> <recipient> [--dry-run]
Arguments
| Argument | Required | Description |
|---|---|---|
<value> | Yes | Amount to send |
<recipient> | Yes | Recipient's owner_hash in hex (with or without 0x prefix) |
Flags
| Flag | Description |
|---|---|
--dry-run | Generate the proof and print it, but don't submit to Soroban |
What It Does
- Loads the wallet and parses the secret key, owner hash, and recipient address.
- Selects the first unspent on-chain note with
value >= <value>. - Fetches the Merkle proof from the indexer (
GET /v1/proof/{index}). - Builds two output notes: recipient note (with
<value>) and change note (with remaining balance). - Runs Groth16 setup with deterministic
seed=42and generates the ZK proof. - Serializes the proof and public inputs for Soroban.
- Computes the new Merkle root (appending both output commitments).
- Submits
transfer(proof, old_root, nullifier, cm_0, cm_1, new_root)to the r14 transfer contract. - Marks the consumed note as spent and adds two new output notes to the wallet.
Output
With --dry-run:
{
"proof": {
"a": "aabb...",
"b": "ccdd...",
"c": "eeff..."
},
"public_inputs": [
"old_root_hex",
"nullifier_hex",
"cm_0_hex",
"cm_1_hex"
],
"nullifier": "0x5e6f7a...",
"out_commitment_0": "0x1a2b...",
"out_commitment_1": "0x3c4d..."
}
Note Selection
The CLI auto-selects the first unspent note matching these criteria:
spent == falsevalue >= requested amountindex.is_some()(confirmed on-chain)
If no matching note exists, the command fails with:
Run r14 balance to sync notes with the indexer first, ensuring index fields are populated.
Config Validation
Requires stellar_secret, core_contract_id, and transfer_contract_id to be configured unless --dry-run is used.