Rust SDK
Two crates hold notes for a Rust program:
worgl-clienttalks to a server.Clientmakes one typed call per route, andPurseruns a holder over HTTP and saves it after every change. Most programs need only this.worgl-sdkis 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.
Add them
Section titled “Add them”cargo add worgl worgl-sdk worgl-clientcargo add tokio --features macros,rt-multi-threadworgl-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.
A payment with a purse
Section titled “A payment with a purse”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.
What else a purse does
Section titled “What else a purse does”| 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(¬es) |
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(¬es) |
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.
The holder on its own
Section titled “The holder on its own”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.
- The payee calls
Holder::requestand sends thePaymentRequest. - The payer calls
Holder::payand returns thePayment. - The payee checks it with
PaymentRequest::check, submits each transfer at its time, takes the new notes in withHolder::absorb, and hands the rest of the answers,Holder::theirs, back to the payer: its change. - 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 timesHolder::resendgives and records each answer withHolder::settle, which works while the mint keeps its answers, and keeps the payee’s notes from it inHolder::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.
Reference
Section titled “Reference”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