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.