Skip to main content
A contract is a RequestScript contract deployed to a running Stellarium node. Its latest version becomes one of the node’s resources, so any node on the network can call it, and it can call any resource itself. You write it in RequestScript instead of TypeScript, and you can deploy it to another organization’s node with a run token, without running a node of your own.

Writing a contract

A contract for Stellarium needs two things a plain RequestScript contract doesn’t:
  • A path, such as com.example, so its full name is unique on the network.
  • A return type, declared after the parameters. It needs RequestScript 1.4.0 or later. The node refuses a contract without one.
Outlook.req
The contract’s body can bind and call any resource the node knows about, including other contracts.

Deploying a contract

Send the source to the node’s POST /v1/contracts, with the node’s API_KEY or a run token from one of your organization’s API clients for that node:
Response
  • Deploying may cost a fee. A node sets what other organizations pay to deploy to it with contractDeployFeeMicroUsd. The fee is paid from your API client’s run, like a paid call, and isn’t charged if the deploy is refused. Deploys with the node’s API key, or by the node’s own organization, are free.
  • Peers learn about it right away. The node announces its resources to its peers again after every deploy and delete, so they can call the contract without restarting.
  • Names are taken once. A contract can’t use the name of a resource registered on the node, or of a resource the node learned from a peer.

Versions and ownership

Versions are immutable, as in RequestScript versioning. 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 with 409. Calls always run the latest version. The first organization to deploy a contract owns every version of it. Only that organization’s API clients can add versions or delete it, with DELETE /v1/contracts/:ref. The node’s API key can do both for any contract on the node.

Calling a contract

A script binds a contract like any resource and calls run with the contract’s parameters:
Today.req
Response
In GET /v1/resources, a contract’s function has kind: "contract" and a paymentManifest with priceMicroUsdPerMillionTokens and recipientOrganizationId.
  • A contract that fails while running, for example by dividing by zero, comes back as the call’s error. Its tokens are still charged.
  • Contracts can call other contracts, at most 8 deep. A deeper call fails, so contracts that call each other in a loop stop.
  • A contract called from a script on the same node reads that script’s run token as the built-in variable jwt. Called from another node, jwt is null. Every contract reads caller, whoever called it.

Keeping state between calls

A contract can keep values between calls in state variables (RequestScript 1.5.0 or later), declared with var state at the top level of its body. Only the contract’s own code can change them, so its published source is also the policy for its data.
Visits.req
  • var state name = value is one copy shared by every caller. var state(scope) name = value keeps a copy for each value of scope, any expression that evaluates to a string or an integer. The initializer runs the first time a copy is used. See State variables for the language rules.
  • caller is a built-in variable every contract reads: the organization paying for the call and the node that made it, both verified. var state(caller.org) ... gives each paying organization its own copy, which no other caller can reach.
  • State is saved only if the call succeeds. A call that fails, or would grow a scope past the node’s maxContractStateBytes (64 KiB by default), saves nothing. Its tokens are still charged.
  • Calls to a stateful contract run one at a time on its node, so concurrent calls never lose an update. A call waits at most 30 seconds for earlier ones, and fails if it has to wait longer. A contract that calls itself, directly or through other contracts, fails its outer call rather than lose the inner call’s update.
  • State belongs to the contract, not a version. A new version keeps it, and a stored value is converted to the new version’s declared type. A value that doesn’t fit fails the call. Deleting every version of a contract deletes its state.
  • State is private. A contract shares it only through what it returns. Its owner and the node’s admin can read all of it with GET /v1/contracts/:ref/state.

How a call is paid

A contract runs on a node that may not have written it, and it may make paid calls of its own. So every call follows one rule: the caller pays for everything the contract does, never the node hosting it.
  1. The caller’s node signs a contract voucher (kind: "contract") for the call, with the node’s advertised price and maxAmount, the most the call may cost. maxAmount is the caller’s contractCostLimitMicroUsd. A call made from a billed run also carries the run’s runId.
  2. The hosting node refuses a call without a contract voucher with 402. It checks that the voucher is bound to this exact call, pays its own organization, agrees to at least its current price, hasn’t expired, and is signed by the voucher’s key. If any check fails it responds 400.
  3. The node asks the operator to authorize a run for the call. The operator holds up to maxAmount on the caller organization’s available balance or, for a voucher with a runId, out of that run’s hold. It refuses the call if the hold can’t cover the contract’s most tokens at the node’s price. If the caller is in the hosting node’s own organization, the call isn’t billed.
  4. The contract runs. Each paid function it calls is paid with a voucher naming the contract’s run, exactly as a billed script’s calls are.
  5. The node captures the run with the tokens the contract executed, charged at the node’s price, and never at more than the price the caller signed. What’s left of the hold goes back where it came from: the caller’s balance, or the caller’s run.
The operator’s refusals reach the caller as the call’s error, with the operator’s status, as for any paid call: see How a paid call works.
A contract that makes paid calls costs more than a single call. contractCostLimitMicroUsd defaults to costLimitMicroUsd, which covers one call, so raise it on nodes that call contracts:
.env
A node learns its listed price from the operator when it starts, and checks it again every five minutes. When the price changes, the node announces its contracts again with the new price.

Hosting contracts

Any node accepts contracts with its API key. To let other organizations deploy, list the node and enroll it in an organization, as for API clients. These configuration options control what they can deploy: Contracts and their state are stored in the node’s database, SQLite or Postgres, and survive restarts.