Environment & Invocation
Access runtime information about the current call and execution environment.
Invocation
Get information about the current endpoint call:
| Method | Returns | PISA target | Description |
|---|---|---|---|
Invocation.ID() | id Identifier | all | Unique ID of this invocation |
Invocation.Caller() | caller Identifier | 0.5.0 and later | Immediate caller (differs from Sender in cross-logic calls) |
Both methods take no arguments. Passing one is a compile error: "invalid argument ID takes exactly 0 argument(s) (), found 1".
Sender vs Caller
Sender— The actor who initiated the interaction (stays the same through cross-logic calls)Invocation.Caller()— The immediate caller (changes in cross-logic calls)
endpoint CheckCaller() -> (sender Identifier, caller Identifier):
sender = Sender // Original actor
caller = Invocation.Caller() // Immediate caller (could be another logic)
Environment
Access runtime environment data:
| Method | Returns | PISA target | Description |
|---|---|---|---|
Environment.Timestamp() | timestamp U64 | all | Current interaction timestamp, in nanoseconds |
Environment.EffortCapacity() | effort_capacity U64 | 0.4.0 and later | Total fuel available |
Environment.EffortAvailable() | effort_available U64 | 0.4.0 and later | Remaining fuel |
Environment.VolumeCapacity() | volume_capacity U64 | 0.4.0 – 0.7.1 | Total storage space |
Environment.VolumeAvailable() | volume_available U64 | 0.4.0 – 0.7.1 | Remaining storage space |
Environment.StorageResult(account_id, payer_id) | added U64, removed U64 | 0.8.0 only | Storage bytes added and removed in this interaction |
endpoint GetEnvInfo() -> (
timestamp U64,
fuel_remaining U64,
invocation_id Identifier
):
timestamp = Environment.Timestamp()
fuel_remaining = Environment.EffortAvailable()
invocation_id = Invocation.ID()
VolumeCapacity() and VolumeAvailable() are not supported on 0.8.0 targets — v0.8.0 meters
storage per account and payer instead of globally, and StorageResult() replaces them. On a
0.8.0 target they're compile errors ("VolumeCapacity is not a valid environment function";
v0.9.1 misspelled it as "envrionment"). StorageResult() in turn is not available on earlier
targets ("not implemented in PISA v0.7.1, implemented in v0.8.0").
Environment.* calls are checked at compile time: the number of arguments, their
names and their types must match the method. Passing a U64 where an
Identifier is expected, for example, reports "type mismatch: argument 'payer_id' of
'StorageResult' expects type identifier, found u64".
Timestamp()
Environment.Timestamp() returns the interaction's timestamp in nanoseconds since the Unix
epoch. A current value looks like 1789365536769825000, not 1789365536. Durations you add to it
or compare it against must be in nanoseconds too:
const NS_PER_SECOND U64 = 1000000000
endpoint Deadline(seconds U64) -> (deadline U64):
deadline = Environment.Timestamp() + seconds * NS_PER_SECOND
The PISA runtime defines the timestamp in nanoseconds. Before v0.9.1, Cocolab supplied seconds to
0.8.0 targets, so lab tests written against seconds are off by a factor of 10⁹. Logics compiled
for 0.5.0 – 0.7.1 targets still get seconds in Cocolab.
Argument names
Arguments of Environment.* calls, Builtins.* calls and
actor methods may be passed with or without a label. When
an argument carries a name, it must match the parameter at that position:
| Argument | Carries a name? | Result |
|---|---|---|
Literal, cast, field access or keyword — "tier", Identifier(Token), p.id, Sender | No | Always accepted |
Labelled — payer_id: x | Yes, the label | Label must equal the parameter name |
Bare variable — acct | Yes, the variable's own name | Variable name must equal the parameter name |
The last row is easy to trip over. A bare variable isn't an unnamed argument, because its name is taken as the argument name:
endpoint dynamic Rename(name String, acct Identifier, pay Identifier) -> (added, removed U64):
mutate name -> Token.Logic.name
// ERROR: argument 0 of 'StorageResult' expected name 'account_id', found 'acct'
// added, removed = Environment.StorageResult(acct, pay)
added, removed = Environment.StorageResult(account_id: acct, payer_id: pay)
Labels are matched by position, not used to reorder arguments, so
StorageResult(payer_id: a, account_id: b) is an error as well. A variable that already has the
parameter's name needs no label.
v0.9.0 ignored argument names of Environment.* calls. Calls that pass literals and keywords, such
as StorageResult(Identifier(Token), Sender), compile unchanged. Calls that pass variables with
other names need labels. A label produces identical bytecode, so adding one doesn't require
redeploying the logic.
Named outputs
Since v0.9.2, Environment.*, Invocation.*, Builtins.*
and asset.* calls accept the same capture syntax as
user functions: (outputs) <- Superglobal.Method(args).
The capture is optional. The plain form still assigns the results by position, to variables of any
name:
memory ts = Environment.Timestamp() // plain: any variable name
memory ts = (timestamp) <- Environment.Timestamp() // capture: the output name is checked
a, r = (added, removed) <- Environment.StorageResult(Identifier(Token), Sender)
added, _ = (added, removed) <- Environment.StorageResult(Identifier(Token), Sender)
memory c = (caller) <- Invocation.Caller()
h = (hash) <- Builtins.Sha256(data)
valid = (ok) <- Builtins.Sigverify(data, signature, pubkey)
b = (balance) <- asset.BalanceOf(token_id: 0, address) // asset logics only
The output names are:
| Call | Output names |
|---|---|
Environment.Timestamp() | timestamp |
Environment.EffortCapacity() / EffortAvailable() | effort_capacity / effort_available |
Environment.StorageResult(account_id, payer_id) | added, removed |
Environment.VolumeCapacity() / VolumeAvailable() (0.4.0 – 0.7.1) | volume_capacity / volume_available |
Invocation.ID() / Caller() | id / caller |
Builtins.Sha256 / Keccak / Blake2b | hash |
Builtins.Sigverify | ok |
asset.BalanceOf | balance |
asset.Symbol / Creator / Manager / Decimals | symbol / creator / manager / decimals |
asset.MaxSupply / CirculatingSupply / EnableEvents | max_supply / circulating_supply / enable_events |
asset.GetStaticMetadata / GetDynamicMetadata / GetStaticTokenMetadata / GetDynamicTokenMetadata | value |
Asset write methods (Transfer, Mint, …) | None |
When you write the capture list, it must match the method exactly:
| Mistake | Error |
|---|---|
| Wrong name | "invalid argument 'Timestamp' returns value named 'timestamp' at position 0, used output 'tstamp'". Builtins.* and asset.* use the same message |
Wrong name on Invocation.* | "invalid argument 'Caller' returns value named 'caller', used output 'who'" |
| Too few or too many names | "invalid argument 'StorageResult' returns 2 value(s), but 1 output(s) were captured" |
To discard an output, list every name in the capture and discard on the left:
a, _ = (added, removed) <- Environment.StorageResult(...).
StorageResult()
Returns two values: how many storage bytes the current interaction has added and removed so far, for one storage account charged to one payer.
added, removed = Environment.StorageResult(account_id, payer_id)
| Argument | Type | Meaning |
|---|---|---|
account_id | Identifier | Whose storage is being measured |
payer_id | Identifier | Who is being charged — matches the payer clause on the write |
coco Token
state logic:
name String
endpoint deploy Init(name String) -> (added, removed U64):
mutate name -> Token.Logic.name payer Logic
added, removed = Environment.StorageResult(Identifier(Token), Identifier(Token))
// Init(name: "noname") -> added = 39, removed = 0
endpoint dynamic Clear() -> (added, removed U64):
mutate "" -> Token.Logic.name // payer Sender is the default
added, removed = Environment.StorageResult(Identifier(Token), Sender)
// added = 1, removed = 7
The first write to a storage key costs 32 bytes for the key plus the encoded value, so storing
"noname" (7 bytes when polorized) into the empty field reports added = 39. Later writes to an
existing key are charged only for the change in content: clearing the field replaces the 7-byte
value with a 1-byte one.
StorageResult() reports the running total at the point where it is called, so call it after the
mutate you want to measure. Both values are returned together. To keep only one, discard the other
with _:
endpoint dynamic ClearName() -> (removed U64):
mutate "" -> Token.Logic.name
_, removed = Environment.StorageResult(Identifier(Token), Sender)
Before v0.9.2, the optimizer at -O2 (the default for coco compile and coco test) deleted the
whole call when only one of its outputs was used, and the endpoint failed at runtime with
builtin.CallFailure::invalid outputs.
Example: Time-based Logic
coco TimeLock
const NS_PER_SECOND U64 = 1000000000
state actor:
locked_until U64
endpoint dynamic Lock(seconds U64):
memory unlock_time = Environment.Timestamp() + seconds * NS_PER_SECOND
mutate unlock_time -> TimeLock.Sender.locked_until
endpoint dynamic Withdraw():
observe locked <- TimeLock.Sender.locked_until:
if Environment.Timestamp() < locked:
throw "Still locked"
mutate 0 -> TimeLock.Sender.locked_until
// proceed with withdrawal