Contract
The Contract class allows you to create objects whose properties can only be updated through its methods. This enables the development of smart contracts in JavaScript. For details, please see here.
If a class C extends from Contract and if c is an instance of C then an error is thrown if
- a property of
cis created, updated or deleted outside of a method ofc - a property
_id,_revor_rootofcis created, updated, or deleted.
In order to provide the two guarantees above we also need to forbid assigning to this inside of constructors. Instead, an object can be passed into super to initialize an object.
Examples
Updating Properties Outside of Methods
class C extends Contract {
constructor() {
super({ n: 0 })
}
set(n: number) {
this.n = n
}
}
const c = new C()
// Assigning n through a method works
c.set(1)
expect(() => {
// Assigning to property outside of a method throws an error
c.n = 2
}).to.throw("Cannot set property 'n' directly")
Updating _id, _rev or _root
class C extends Contract {
set(rev) {
this._rev = rev
}
}
const c = new C()
expect(() => {
// Assigning a provenance property throws an error, even inside a method
c.set('rev')
}).to.throw('Cannot set _rev')
Using the Initialization Object
class C extends Contract {
n: number
constructor() {
// Use the initialization object
super({ n: 0 })
}
}
const c = new C()
expect(c.n).eq(0)
Querying inside of a Contract
Smart contracts can access a restricted global computer (the InnerComputer) to read on-chain history and block metadata. These helpers do not write blockchain state. They exist so contracts can traverse revision graphs, load modules, or base decisions on confirmed chain data in a deterministic way.
Architecture (SES sandbox, eval-frame invalidation, hardened endowment, client vs contract computer) is documented in Sandbox & Inner Computer.
The outer Computer client API is not the same surface: many client methods may return undefined for mempool data or live tips. Inside a contract, almost every “not yet known / not yet confirmed” observation invalidates the whole evaluation so two validators can never disagree under chain extension.
Determinism property (observation stability)
Let b₁ and b₂ be chain states such that b₂ extends b₁ (every block and transaction present in b₁ is also in b₂).
For any InnerComputer method m and arguments args:
If
computer.m(...args)succeeds without invalidation againstb₁, then the same call againstb₂must succeed and return the same value.
Successful observations must therefore be invariant under future chain growth. Transient facts (mempool-only txs, “no next revision yet”, unspent tip as “last”, future block heights) must not become part of a valid transition.
How invalidation works
- On a forbidden observation, InnerComputer marks the current evaluation frame invalid and throws. There is no per-instance invalid flag. Each
Db.eval/Modules.loadbinds a frame viawithEvalInvalidation:- Node:
AsyncLocalStorage(loaded without a staticnode:async_hooksimport so browser bundles stay clean). Concurrent evals are isolated by async context. - Browser: await-scoped stack with serialized roots (no Promise patching under SES
lockdown). Nested loads still nest; concurrent root evals queue.
- Node:
- The host installs the active observation client on the frame; free-var query methods route to that client for the evaluation. Invalidation always writes the active frame, so catch-and-continue cannot soft-succeed.
- A contract
try/catchcannot clear invalidation — the endowment has no invalidation API. After the compartment returns or throws, the host rejects using only the frame it holds (frame.invalid/frame.msg). - The compartment is endowed with a hardened query-only facade of InnerComputer methods (no internal
Computerclient, noisInvalid/resetInvalid, methods not replaceable). consoleis only available indev/debugmode. Inprod, contracts must not useconsole(it is not in scope →ReferenceError). Logging is not part of the deterministic on-chain API; see Sandbox & Inner Computer.
Error message shape
All invalidation errors exposed to callers end with exactly one copy of:
Accessing non-existent on-chain state inside a smart contract is forbidden.
- Policy rejections (for example, future
getBlockHashheight, missinggetTXOsstabilizer) and missing/unconfirmed observations both go through a shared formatter as soon as invalidation fires. A short reason may appear before the standard suffix; the suffix is never doubled. - Uncaught throws and catch-and-continue paths share this shape: the thrown error and
frame.msgalready carry the single suffix;Db.evalrethrows the same canonical form when the frame is invalid.
Clients and tests should match with message.endsWith(...) (or equivalent). Do not expect a short policy reason alone without the forbidden suffix.
Confirmed locations only
Most location-based APIs require the referenced transaction to be confirmed (in a block) before the call may succeed. Unconfirmed / mempool locations are treated as transient.
App / test implication: after deploy, new, method calls, or delete, wait for confirmation before on-chain code that walks history, loads modules, or calls last / next / txIdToBlockTime on those locations.
Full API reference (InnerComputer)
Aliases getUTXOs, getOTXOs, and getOUTXOs inherit the same rules as getTXOs.
latest is not exposed on InnerComputer (the live tip is inherently non-deterministic under chain extension).
Detailed notes
sync / decode / load
sync(location: string): Promise<any>
decode(txId: string): Promise<TransitionJSON>
load(location: string): Promise<Record<string, any>>
syncdeep-clones the object (BigInt-safe) so contracts cannot mutate live graph state.decoderequires a confirmed Bitcoin Computer transaction.loadaccepts a module rev (txId:outputIndex); the deploy transaction must be confirmed.- Missing or unconfirmed targets invalidate the evaluation.
getAncestors
getAncestors(location: string): Promise<string[]>
- Starting location must be confirmed.
- An empty array is a stable result when there are no ancestors; it does not require special “allow null” handling (empty arrays are not nullish).
first / prev / next / last
first(rev: string): Promise<string>
prev(rev: string): Promise<string | undefined>
next(rev: string): Promise<string>
last(rev: string): Promise<string>
first: starting rev must be confirmed; returns the creation revision.prev: starting rev must be confirmed.undefinedat the confirmed root is stable and does not invalidate. This is the only nullish success path among these helpers.next: “no successor yet” is transient → invalidates. A mempool-only successor also invalidates; the returned next rev must be confirmed.last: does not mean “current unspent tip”. An unspent tip yieldsundefinedfrom the underlying API and invalidates. A definite last is the tip of a lineage whose tip is spent in a confirmed transaction (e.g. after a confirmeddelete). Use this for terminal escrow checks, not for reading the live tip.
Block and time helpers
txIdToBlockTime(txId: string): Promise<bigint>
txIdToBlockHeight(txId: string): Promise<number>
txIdToBlockHash(txId: string): Promise<string>
getBlockHash(height: number): Promise<string>
getBlockHeight(hash: string): Promise<number>
- Unconfirmed transactions cannot be observed as stable times/heights/hashes.
getBlockHashrejects negative heights and heights greater than the current tip (future blocks are non-deterministic).
getTXOs (and aliases)
getTXOs(q: TXOQuery): Promise<string[] | TXORecord[]>
Inside a contract the query must include one stabilizing filter:
lteBlockHeight— must be ≤ current tip (not in the future)blockHeight— must be ≤ current tipblockHash— fixed historical block
Queries without a stabilizer, or with a future/negative height, invalidate. Empty result sets with a valid stabilizer are fine (indexing lag is an application concern, not invalidation).
Usage notes & best practices
- Confirm before query. Deploy modules, create objects, update or delete tips, then wait for confirmation before contract methods that call InnerComputer on those locations.
- Prefer
prev/getAncestors/firstfor history walks. Usenextonly when a confirmed successor must exist (e.g. deposit pre/post pair). - Do not treat
lastas “latest live tip”. For terminal claims, spend the tip (e.g.delete) and wait for confirmation, then calllast. try/catchdoes not soft-fail invalidation. Catching the throw still rejects the transition if the evaluation frame is invalid. Public errors always end with a single “Accessing non-existent…” suffix (policy reasons and missing locations alike).- Stabilize TXO queries with
lteBlockHeight,blockHeight, orblockHash. - Off-chain code using the outer
Computermay still see mempool data; only the in-contractcomputerglobal enforces these rules. - Escrow / multi-step apps: after cancel, settle, or
delete, wait for confirmation before a follow-up contract call that depends onlast, deposit deltas vianext, or confirmed history (see Sandbox & Inner Computer – Practical implications).
This API, together with Contract property rules, enables verifiable on-chain logic while keeping evaluations fail-closed under non-deterministic observations.