Skip to content

Rust SDK

Two crates hold notes for a Rust program:

  • worgl-client talks to a server. Client makes one typed call per route, and Purse runs a holder over HTTP and saves it after every change. Most programs need only this.
  • worgl-sdk is the holder’s side with no networking and no files: a note store that requests, pays, receives and refreshes notes while keeping the privacy rules. It runs anywhere, WebAssembly included.
cargo add worgl worgl-sdk worgl-client
cargo add tokio --features macros,rt-multi-thread

worgl-client runs its requests on Tokio, over rustls with the platform’s certificate roots. worgl brings the shared types, such as PublicKey for a mint’s key and Note.

use worgl_client::{Client, Purse};
use worgl_sdk::Spacing;
let client = Client::new("https://mint.example.com")?;
let alice = Purse::open(client.clone(), "alice.json")?;
let bob = Purse::open(client, "bob.json")?;
// Bob asks for 5 and sends the request to Alice.
let request = bob.request(&mint, 5).await?;
// Alice answers with signed transfers and sends them to Bob.
let payment = alice.pay(&request, Spacing::default()).await?;
// Bob checks them, submits each at its time and keeps the notes.
let received = bob.receive(&request, &payment).await?;
assert_eq!(received.value, 5);
// Bob sends Alice the rest of the answers, her change, and she keeps it.
alice.accept(&received.theirs).await?;

This is the example in worgl-client’s crate documentation, which CI compiles. A Purse wraps a holder, a Client and a Store. It reads mint info by itself, submits each transfer at its time, retries what fails on the way (a resent transfer gets the same answer), and saves after every change, before handing out anything that depends on it. After a busy answer it waits as long as the mint’s Retry-After asks, up to 30 seconds, before the next try.

Purse::open keeps the purse in a JSON file. The file holds every note’s secret key, and the notes are bearer, so keep it private and encrypted at rest. Implement Store to keep it elsewhere; the worgl command keeps its purse encrypted under a passphrase this way. Several processes can share the file: before each change a purse locks it and reads it again, so none saves over another’s change. A Store of your own does the same by implementing lock.

The mint’s answers to a payment hold notes for both sides, and Submitted::theirs lists the other side’s. The payee hands them back to the payer: they are the payer’s change, which Purse::accept checks against the mint’s keys, keeps, and uses to settle the payer’s transfer. When the payer submits instead, with Purse::submit, it hands theirs to the payee, who keeps its notes with Purse::accept the same way. The payer’s purse keeps them for the payee too, until the payer calls Purse::forget_owed with them once they are handed over, as worgl submit does. Purse::take_from_faucet asks a playground faucet for a note, to a fresh key like any other.

Call What it does
request(&mint, amount) Asks to be paid: a PaymentRequest of fresh keys to send the payer.
request_at_most(&mint, amount, largest) The same, in notes no larger than largest.
pay(&request, spacing) Answers a request with a Payment of signed transfers. Refuses, before signing, a request for a note the mint does not make or to a key the purse has paid already.
receive(&request, &payment) Checks a payment, submits each transfer at its time and keeps the notes.
settle(spacing) Sends the transfers not yet seen answered again, and takes in the change.
cancel(&transfer) / forget(&transfer) Ends an outgoing transfer: cancel takes back the note of one the mint never applied, forget drops one whose note is gone. Both keep its change keys, so change handed over later is still taken in.
holder().payments() / forget_owed(&notes) The payments the purse made and keeps, each with the Payment to send again (less any transfer cancelled or forgotten) and the payee’s notes from every answer the purse got itself, by settling, refreshing or submitting; then stops keeping those notes once they are handed over.
submit(&transfers) / accept(&notes) One side submits; the other keeps what it is handed, and settles the transfers it answers.
withdraw(&request) Stops expecting the notes of a request that will not be paid.
balance(&mint) What the purse holds at a mint, by what it can do now.
refresh(&mint) Pays the notes that die at the next rotation back to the purse, each at a random moment before it by the server’s clock, and resends the transfers not yet seen answered that must land before it. Returns once the last is done, which may be most of a period away.
plan_refresh(&mint) The same plan, for an application with its own scheduler to submit.
prune(&mint) Drops notes whose layer the mint’s info says it has dropped, and returns the value lost. This machine’s clock plays no part.

A resent transfer gets the same notes only while the mint keeps its answer (see Error codes), and at the latest until those notes expire. After that, a transfer already applied is refused as spent or expired, and Refused::at_stake says it was possibly applied and what may be lost: the change can then only come from the counterparty, for accept. The purse drops such a transfer, keeping its change keys. It cancels one refused as layer_full, which the mint never applied and never will, and one refused as policy that the policy the mint publishes refuses too, taking its note back; Refused::outcome says what it did (see Error codes). refresh resends the transfers whose spent note dies at the next rotation, or died at the last one. The answer to a payment’s transfer keeps the payee’s notes, in MadePayment::owed, for the payer to hand over: the purse cannot tell whether the payee submitted the payment or the resend carried it out.

Client errors keep the mint’s stable code, such as spent or frozen, in Error::code, Error::is_transient says whether trying again later may work, and Error::retry_after how long a busy mint asked to wait first. is_transient counts layer_full out: a transfer refused so is refused every time, though a mint request may pass after the next rotation. See Error codes.

Spacing sets the seconds between transfers that belong together, as a range the purse picks from at random. Spacing::default() spreads them; a closer spacing lets the mint guess the notes had one owner. See the Privacy model.

use worgl::{Issuer, IssuerKey, LedgerConfig, Policy};
use worgl_sdk::{Holder, MintInfo, MintView, Spacing};
// A mint in memory, standing in for the server.
let ledger = LedgerConfig { layers: 4, layer_size: 1 << 16 };
let mut mint = Issuer::new(IssuerKey::generate(), ledger, Policy::cash_like())?;
let view = |mint: &Issuer| {
MintView::new(&MintInfo {
issuer: *mint.public_key(),
layers: mint.layers(),
policy: mint.policy().clone(),
rotation: None,
paused: false,
})
};
// Alice is minted 8 in one note.
let (mut alice, mut bob) = (Holder::new(), Holder::new());
let funding = alice.request(&view(&mint)?, 8)?;
let note = mint.mint(8, funding.outputs[0].holder)?;
alice.absorb(&[note]);
// Bob asks for 5, Alice pays, Bob checks and submits.
let request = bob.request(&view(&mint)?, 5)?;
let payment = alice.pay(&view(&mint)?, &request, Spacing::default())?;
request.check(&payment)?;
for transfer in &payment.transfers {
let answer = mint.transfer(&transfer.request)?;
bob.absorb(&answer);
// Bob hands Alice the rest, her change; she checks it and keeps it.
let change = bob.theirs(&answer);
for note in &change {
note.verify(mint.keys())?;
}
alice.take_answers(&change);
}
// Bob's 5 is frozen until the next rotation; Alice has 3 in change.
assert_eq!(bob.balance(&view(&mint)?).frozen, 5);
assert_eq!(alice.balance(&view(&mint)?).frozen, 3);
assert_eq!(alice.outgoing().count(), 0);

This example, from worgl-sdk’s crate documentation, runs a mint in memory in place of the server. CI runs it.

  1. The payee calls Holder::request and sends the PaymentRequest.
  2. The payer calls Holder::pay and returns the Payment.
  3. The payee checks it with PaymentRequest::check, submits each transfer at its time, takes the new notes in with Holder::absorb, and hands the rest of the answers, Holder::theirs, back to the payer: its change.
  4. The payer checks their signatures and takes them in with Holder::take_answers, which settles the transfers they answer. Without them, it resends its outgoing transfers at the times Holder::resend gives and records each answer with Holder::settle, which works while the mint keeps its answers, and keeps the payee’s notes from it in Holder::payments, in case the payee has not submitted.

A MintView, read from a mint’s info, tells the holder which notes can move now, which are frozen until the next rotation and which die at it. It holds the info’s layers, which no clock moves, so read the info again after a rotation. Holder::refresh keeps notes alive by paying the ones about to expire to the holder itself, and Holder::due_resends gives the outgoing transfers to resend before the next rotation. Both spread them over the time left from a now by the mint’s clock, which Client::view_and_time reads from the Date of the server’s answer, and plan them all at once when that time has passed the rotation’s. The holder saves to JSON with its secret keys.

The Rust API reference documents every type and call, built from the same crates by cargo doc.

v0.1Not yet independently security reviewed. Don't use it to hold anything you can't afford to lose.MIT or Apache-2.0