The ledger and files in Rust

A Rust function can use the same ledger and file operations as a JavaScript function.

Docs menu

The Rust ledger is substrio_wasm::db::ledger(), corresponding to db.ledger. File operations are in substrio_wasm::files, corresponding to substrio.files. Calls go directly to the platform ledger and file store, without an HTTP request or JSON between the function and the store. The results and error names match the JavaScript calls.

The ledger's entries, its accounts, and its catalog of kinds and categories are described in The ledger. The Rust function itself, its deploy, and its limits are described in Wasm functions (Rust).

The ledger

db::ledger() opens the app's ledger. It returns LEDGER_UNAVAILABLE when the app has no ledger. Kinds and categories are named in the catalog and used by index. Look up an index once with kind(name) or category(name).

Rust call JavaScript equivalent Result
l.kind(name) db.ledger.kind(name) Kind, the kind's index
l.category(name) db.ledger.category(name) Category, the category's index
l.post(&entries) db.ledger.post(entries) Post { outcome, ids }
l.balance(kind, party, currency, org) db.ledger.balance(kind, party, currency, org) Balance { pending, cleared, pending_rows, cleared_rows }
l.top_balances(currency, n) db.ledger.topBalances(currency, n) a list of { account_kind, party, amount }
l.accounts(kind, currency) db.ledger.accounts(kind, currency) a list of { party, amount }
l.totals(value, currency, by) db.ledger.totals(value, currency, by) a list of { account_kind, category, entry_type, status, amount, rows }
l.account_totals(kind, currency) db.ledger.accountTotals(kind, currency) a list of { party, category, entry_type, status, first, amount, rows }
l.declare_totals(by) db.ledger.declareTotals(by) Declared::Declared or Declared::Existing
l.clear(id) db.ledger.clear(id) Change::Changed or Change::Unchanged
l.void(id) db.ledger.void(id) Change::Changed or Change::Unchanged
l.clear_at(id, ms) db.ledger.clearAt(id, ms) Change::Changed or Change::Unchanged
l.annotate(id, notes, tags, by) db.ledger.annotate(id, notes, tags, by) Noted::Changed or Noted::NotFound
l.clear_due(limit) db.ledger.clearDue(limit) ClearDue { cleared, more }
l.reverse(id) db.ledger.reverse(id) Post { outcome, ids }
l.get(id, read) db.ledger.get(id) the entry's row through read, or None
l.id_of(key) db.ledger.idOf(key) EntryId, the id a key's entry has
l.entries(by, value, before, limit, org, read) db.ledger.entries(by, value, before, limit, org) Page { entries, next_before, more }

An argument JavaScript lets you leave out is an Option in Rust. None is the same as leaving it out, and Some("") is an empty string. A by of None is By::Scope. A limit of None is 50.

A list is read in place from the call's result. Its rows borrow party text from the list, so it uses one buffer and makes no allocation per row. get and entries pass each row to your read closure while it is still in the store's memory. Retain only the fields you read.

Entries

An Entry is built with Entry::new(kind, party, currency, amount, entry_type). Its other members are public fields, and .key(key) sets the idempotency key. The text members are borrowed from your strings, and post sends them to the ledger without a copy. data is JSON text, and it must be an object.

use substrio_wasm::db::{self, ledger::{Entry, EntryType}};

let l = db::ledger()?;
let cash = l.kind("cash")?;
let mut sale = Entry::new(cash, "alice", "USD", 500, EntryType::Credit).key("order-1");
sale.reference = "order-1";
sale.data = r#"{"sku":"A1"}"#;
let posted = l.post(&[sale])?;
let balance = l.balance(cash, "alice", "USD", None)?;

Repeating the same post returns Outcome::Existing with the same ids and does not write the entries twice.

Refusals

A ledger refusal is ledger::Error::Refused { reason, entry }. Its reason.name() matches the JavaScript LedgerRefused error's error, and entry matches its index, identifying the refused entry in a post. A write the store could not finish returns ledger::Error::WriteFailed, corresponding to LedgerWriteFailed. Other refusals return ledger::Error::Platform with the platform code. db::last_error_message() contains the same message as the JavaScript error. A wrong argument type throws TypeError in JavaScript. Rust's types rule out most such arguments; the rest return INVALID_ARGUMENT.

Reason Code When
ACCOUNT_KIND_UNKNOWN 1 An entry's kind is not in the catalog.
CATEGORY_UNKNOWN 2 An entry's category is not in the catalog.
CURRENCY_INVALID 3 A currency is not three capital letters.
AMOUNT_ZERO 4 An entry's amount is zero.
NOT_BALANCED 5 A balanced post does not sum to zero.
WRITE_FAILED 6 The store could not write the post.
ORIGINAL_NOT_FOUND 7 The entry a reversal names does not exist.
ORIGINAL_VOIDED 8 The entry a reversal names is voided.
ORIGINAL_IS_REVERSAL 9 The entry a reversal names is a reversal itself.
KEY_REQUIRED 10 An entry has no key.
CREATED_AT_REQUIRED 11 An entry has no creation time.
ORIGINAL_KEY_REQUIRED 12 A reversal names no original.
ORIGINAL_KEY_UNEXPECTED 13 An entry that is no reversal names an original.
ID_INVALID 32 An id is not an entry id.
LIMIT_INVALID 33 A limit is zero or too large.
NOT_FOUND 34 No entry has the id.
WRONG_STATUS 35 The entry's status does not allow the change.
TOTALS_NOT_DECLARED 36 The totals asked for were never declared.
TYPE_INVALID 40 An entry's type is not one of EntryType, or is a reversal in a post.
STATUS_INVALID 41 An entry's status is not one a post may set.
DATA_INVALID 42 An entry's data is not a JSON object.
AMOUNT_INVALID 43 An amount is larger than 2^53 - 1.
CLEAR_AT_INVALID 44 A clearing time is larger than 2^53 - 1.

The platform codes these calls reach are LEDGER_UNAVAILABLE (the tenant has no ledger), INVALID_ARGUMENT (an unknown kind or category name, or a value past 2^53 - 1), and NO_CALL_CONTEXT (a call made outside a request).

Files

Rust call JavaScript equivalent Result
files::put(path, data, &options) substrio.files.put(path, data, options) Meta
files::stat(path) substrio.files.stat(path) Some(Meta), or None when there is no file
files::get(path) substrio.files.get(path) Some(bytes), or None
files::get_into(path, &mut buf) substrio.files.get(path) true with the bytes in buf, or false
files::remove(path) substrio.files.remove(path) true when a file was removed
files::response(path, status, range) substrio.files.response(path, { status, range }) Some(Response), or None
files::slice(path, offset, length, status) substrio.files.slice(path, offset, length, { status }) Some(Response), or None
files::fetch(url, path, &options) substrio.files.fetch(url, path, options) the job's id, 16 hex digits

Meta has size(), created_at(), updated_at(), public(), content_type(), cache_control(), and hash(). get reads the file straight into your function's memory in one read. get_into reuses a buffer you keep, so a function that reads many files allocates once.

response and slice return a Response that serves the file. Its bytes go from the file to the client without passing through your function. A range header such as bytes=0-4 returns 206 with Content-Range, and a range past the end returns 416, as in JavaScript. You may add headers to the response before you return it.

use substrio_wasm::{files, Ctx, Env, Request, Response};

fn fetch(req: &Request, _env: &Env, _ctx: &Ctx) -> Response {
    match files::response("docs/report.pdf", 200, req.header("range")) {
        Ok(Some(res)) => res,
        Ok(None) => Response::text(404, "not found"),
        Err(_) => Response::text(500, "unavailable"),
    }
}

A refused file call returns a db::Error with the platform code, and db::last_error_message() contains its message. An invalid file path returns INVALID_ARGUMENT. fetch returns GRANT_REQUIRED unless the app has the fetch_to_file grant, and QUEUE_NOT_DECLARED when its queue is not declared. A header containing a line break returns INVALID_ARGUMENT.

Cost

Each call is one import into the platform. A list call is two imports when its answer does not fit the buffer it starts with, and a buffer that grew is kept for the next call. A refusal's reason is read with one more import, only when a call is refused. Nothing a call reads is copied into an intermediate form on its way to you.

See also Rust functions, the entry page: when to pick Rust, every call exposed to it, deploy and testing.