Build / Contracts

Contracts

A contract is a RequestScript script you deploy to a node. It becomes one of the node's resources, so anyone on the network can call it, and it can call any resource itself. You don't write TypeScript or run your own node, and every call is paid for by whoever made it.

Language
RequestScript 1.4+
Deploy
POST /v1/contracts
Function
run
Price
The node's price per million tokens

01

What is a contract?

Most resources on Stellarium are TypeScript objects that a node's owner registers before the node starts. A contract is a resource written in RequestScript instead, and deployed to a node that's already running, by its owner or by another organization.

Registered resourceContract
Written inTypeScriptRequestScript
Added bythe node's owner, at startupanyone with a run token, at any time
Functionsany you declareone, run, taking the contract's parameters
Priceper call, per token, or freethe tokens it runs, at the node's price
Codestays private on the nodepublic: anyone can read what a call does

To a script, a contract looks like any other resource. Peers learn about it as soon as it's deployed.

02

Write one

A contract is a RequestScript contract with two requirements:

  • A path, such as com.example, so its full name is unique on the network.
  • A return type after its parameters. The value it returns is checked against it, and callers see it in the resource listing.
Outlook.req
contract com.example.Outlook(city: string): string {
  const weather: com.example.Weather
  const today = weather.temperature(city: city)
  if (today.error != null) {
    return "unknown"
  }
  return "${today.value} degrees"
}

This contract calls the paid com.example.Weather resource and turns its answer into words. Like any script, it checks error on a call's result, so a failed call doesn't fail the contract.

03

Deploy it

Send the source to the node's POST /v1/contracts, with a run token from one of your organization's API clients for that node. Create the API client on the Stellarium Platform. The node's owner can also use its API key.

Terminal
curl -X POST https://node.example/v1/contracts \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $RUN_TOKEN" \
  -d '{"source": "contract com.example.Outlook(city: string): string { ... }", "description": "Today in words"}'
Response
{ "path": "com.example", "name": "Outlook", "version": "1", "feeMicroUsd": 1000 }
  • Deploying may cost a fee. A node sets what other organizations pay to deploy to it. The fee comes out of your API client's run, and isn't charged if the deploy is refused. Deploying to your own organization's node is free.
  • Peers hear about it right away. The node announces its resources again, so other nodes can call the contract without restarting.
  • Names are first come, first served. A contract can't take the name of another resource on the network.

If you run a node, these settings control what others can deploy to it:

.env
# What another organization pays to deploy a contract here, in micro-USD.
CONTRACT_DEPLOY_FEE_MICRO_USD=1000
# The largest contract this node accepts. Defaults to 64 KiB.
MAX_CONTRACT_SOURCE_BYTES=65536

04

Call it

A script binds a contract like any resource and calls its run function:

Today.req
request Today {
  const outlook: com.example.Outlook
  return outlook.run(city: "Oslo")
}
Response
{ "returnValue": { "value": "12 degrees", "error": null }, "usage": { "tokens": 17 } }

Contracts can call other contracts, up to 8 deep, so a loop between them stops. A contract that fails, for example by dividing by zero, comes back as the call's error.

05

How a call is paid

A contract runs on a node that didn't write it, and it may make paid calls of its own. Stellarium makes one rule hold for every call: the caller pays for everything the contract does, never the node hosting it.

01

The caller signs a voucher

Its node signs a contract voucher with the node's advertised price and maxAmount, the most the call may cost. A call made from a billed run carries that run's id too.

02

The host checks it

The voucher must be for this exact call, unexpired, signed by the caller and paying the host's organization. Without one, the call is refused before anything runs.

03

The operator holds the cost

It opens a run for the call and holds up to maxAmount on the caller's balance, or out of the caller's own run. It never charges more than the price the caller signed.

04

The contract runs

Every paid call the contract makes draws on that run, so the host never pays for a caller's contract.

05

The host reports its tokens

Afterwards, the host captures the tokens the contract executed, charged at its price per million tokens. A contract that failed is still charged for what it ran.

06

The rest goes back

Whatever the run didn't spend returns to where it came from: the caller's balance, or the caller's run.

A contract that makes paid calls costs more than one call, so a node that calls contracts can raise its ceiling:

.env
# The most one call to a contract on another node may cost, with the
# paid calls the contract makes, in micro-USD. Defaults to COST_LIMIT_MICRO_USD.
CONTRACT_COST_LIMIT_MICRO_USD=50000

06

Versions and ownership

  • Versions are immutable. To change a contract, deploy it again with a new version, such as contract com.example.Outlook@2(...). Deploying a version that exists is refused.
  • Calls run the latest version. GET /v1/contracts lists every version with its source, so you can read exactly what a call will do.
  • The first organization to deploy a contract owns it. Only its API clients can add versions or delete it with DELETE /v1/contracts/com.example.Outlook@2. The node's owner can delete any contract on their node.

07

Keep state between calls

A contract can keep values between calls in state variables, declared with var state at the top of its body. Only the contract's own code can change them, so the published source is also the policy for its data: a counter, a quota, a cache, or an agent's memory.

Visits.req
contract com.example.Visits(page: string): int32 {
  var state total: int32 = 0
  var state(caller.org) mine: int32 = 0

  total = total + 1
  mine = mine + 1
  return mine
}
  • var state name is one copy shared by every caller. var state(scope) name keeps a copy for each value of scope, any expression that evaluates to a string or a number.
  • caller tells a contract who it runs for: caller.org is the organization paying for the call, and caller.node the node that made it. Both are verified, so state(caller.org) gives each organization a copy no one else can reach.
  • State is saved only when a call succeeds. A call that fails, or would grow its state past the node's limit (64 KiB a scope by default), saves nothing.
  • Calls to a stateful contract take turns on its node, so concurrent calls never lose an update.
  • State outlives versions. A new version keeps it, and deleting the contract deletes it.
  • State is private. A contract shares what it returns. Its owner and the node's operator can read the rest for debugging.

08

Reference

Contracts appear in GET /v1/resources with kind: "contract" and their price, alongside every other function. Read the full reference in the stellarium-ts README and the documentation, and learn the language at requestscript.org.

Built a contract? Show it off on Discord.