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
Deploying a contract
Send the source to the node’sPOST /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 ascontract 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 callsrun with the contract’s parameters:
Today.req
Response
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,jwtisnull. Every contract readscaller, whoever called it.
Keeping state between calls
A contract can keep values between calls in state variables (RequestScript 1.5.0 or later), declared withvar 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 = valueis one copy shared by every caller.var state(scope) name = valuekeeps a copy for each value ofscope, 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.calleris 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.- The caller’s node signs a contract voucher (
kind: "contract") for the call, with the node’s advertised price andmaxAmount, the most the call may cost.maxAmountis the caller’scontractCostLimitMicroUsd. A call made from a billed run also carries the run’srunId. - 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 responds400. - The node asks the operator to authorize a run for the call. The operator holds up to
maxAmounton the caller organization’s available balance or, for a voucher with arunId, 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. - 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.
- 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.
error, with the operator’s status, as for any paid call: see How a paid call works.
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.