Endpoints and Functions
Endpoints are the externally callable entry points of a Coco logic, while functions are local helpers you call from endpoints. Endpoints define named inputs and named return values, and may be marked dynamic when they mutate state (any block that uses mutate must be in a dynamic endpoint). Special qualifiers deploy and enlist handle one-time logic initialization and per-actor initialization, respectively. Aside from visibility and these qualifiers, endpoint and function syntax is the same; Coco favors explicit, named arguments and returns for clarity and auditability.
Endpoint Syntax
This formal definition breaks down exactly how to write the "header" (signature) of a Coco endpoint.
1. The Master Structure
The generic formula for an endpoint is:
endpoint [Special Tags] Name (Inputs) -> (Outputs)
endpoint deploy dynamic InitSystem(seed: U64) -> (success: Bool)
2. Qualifiers (The "Tags")
These are optional keywords that define when the endpoint runs and what it can do to the memory.
A. Endpoint Qualifiers (deploy | enlist)
Defines if this is an initialization script.
deploy: This is like the Logic's Constructor. It runs once when the logic is first uploaded to the blockchain to initialize the logic stateenlist: This is like a User's Constructor. It runs once per user the first time they interact with the logic. Its job is to initialize that specific user's actor state- (None): If omitted, it is a standard endpoint invokeable anytime
Note: See the Deploy and Enlist section below for a more detailed explanation.
B. State Qualifiers (dynamic | static | pure)
Defines how the endpoint interacts with logic state.
dynamic: Write access to logic or actor state. Required if you usemutateto modify values of the logic or actor state.static: Read-only. Can read but can't change state. Required if you useobserveto read values of the logic or actor state.pure: No access to state. A pure function that can't access state at all, even read. This is the default if you don't specify one.
3. Identity (name & Identifier)
This rule defines what valid names look like for Endpoints, Variables, and Types.
- Rule: Must start with a letter (
A-Zora-z) - Allowed: Letters, numbers, and underscores (
_)
Valid Examples:
Transferget_balanceUser2
Invalid Examples:
2User(Starts with number)Get-Balance(Hyphens not allowed)
4. Inputs and Outputs
In many languages (like Java or C++), you can define a function signature like this: func(String, Int). You know the types, but not what they mean.
Coco forbids this. You must give every input and every output a name immediately in the signature.
Syntax: name: type
- Arguments:
(user: String, amount: U64) - Return Values:
-> (new_balance: U64, success: Bool)
Why? This ensures that when you look at the function signature, you know exactly what the data represents (e.g., id: U64 vs timestamp: U64), not just that it is a number.
5. Data Types (type)
The definition lists what kind of data you can pass in or return.
A. Primitive Types
Basic building blocks:
Bool: True/FalseString: TextU64,I64,U256: Integers (Unsigned and Signed)Bytes: Raw byte dataPtr: Pointers (memory references)Identifier: Blockchain addresses (32-byte)
B. Complex Types
Identifier: Refers to a custom Class or Event (e.g.,Person)Map[Key]Value: Key-Value storage (e.g.,Map[String]U64)[]type: A dynamic array (list) of items (e.g.,[]String)[number]type: A fixed-size array (e.g.,[5]U64)
Summary Example
Putting it all together into a valid Coco signature:
// [Qualifiers] [Name] [Argument: NamedValue] [Return: NamedValue]
endpoint dynamic PurchaseTicket( quantity: U64 ) -> ( receipt_id: String )
endpoint: Keyworddynamic: State Qualifier (it will change memory)PurchaseTicket: Identifier (Name)quantity: U64: Argument (NamedValue)receipt_id: String: Return Value (NamedValue)
coco Functions
endpoint Combine(x,y String) -> (z String):
memory w = join(y, (output) <- DoSomething())
yield z join(x, w)
function DoSomething() -> (output String):
yield output "done!"
Functions and Calls
function keyword is used for callables that can only be called from endpoints locally. They
can't be invoked on the blockchain. On the other hand, endpoint can't invoke another endpoint in the same logic
so function is a conventional reusable encapsulation of some functionality. So the difference between endpoint and function is just their visibility (endpoints can be called from blockchain and function from endpoints), everything else in the syntax is the same.
| Feature | endpoint | function |
|---|---|---|
| How It's Called | Externally, from the blockchain. It's the "externally callable entry point." | Locally, only from an endpoint within the same logic. It's a "local helper." |
| Main Purpose | The main public-facing interface for the logic. | To be a "reusable encapsulation of some functionality" for endpoints to use. |
| Initialization Qualifiers | Yes. Can use deploy (for the logic) and enlist (for actors). | No. These qualifiers are specific to endpoints. |
| Invocation Rule | Cannot invoke another endpoint in the same logic. | Can be called by any endpoint in the same logic. |
Function Call Syntax
The syntax for calling functions in Coco is uniquely verbose to maximize readability and auditability. It requires explicit naming of arguments and return values.
Formal Syntax:
[(return_name,...) <-] [scope::]<name> ( [argument...] )
Key Rules:
- The Arrow (
<-): This operator captures return values. You must map the function's output names to your local variables (e.g.,(result) <- Func()). If you don't need the result, this part can be omitted - Named Arguments (
name: value): You cannot simply pass a value. You must explicitly name the argument you are filling (e.g.,val: 100) - Shortcut: If your local variable has the exact same name as the argument, you can omit the name label (e.g.,
Double(num)instead ofDouble(num: num)) - Scope (
::): If calling a function from an external package, use the double colon separator (e.g.,Math::Sqrt)
coco FCall
endpoint A():
// We send '10' into the argument 'num'.
// We catch the function's return value 'out' and store it in a new variable 'x'.
memory x = (out) <- Double(num: 10)
memory num = 20
// Because our local variable 'num' has the same name as the function's argument 'num',
// we can just write 'Double(num)' instead of 'Double(num: num)'.
// The same applies to the return: 'out' is caught in a new local variable also named 'out'.
memory out = Double(num)
// The 'Pair' function returns two values, 'x' and 'y'.
// We capture 'x' into our new local 'a', and 'y' into our new local 'b'.
memory a, b = (x, y) <- Pair()
// A simple function that takes 'num' and returns 'out'.
function Double(num U64) -> (out U64):
out = num * 2
// A simple function that returns two hardcoded values.
function Pair() -> (x, y U64):
x, y = 3, 4
There's another strict rule regarding arguments and return values: arguments are read-only and return values are write-only.
// This function demonstrates the strict read/write rules.
function X(a, b U64) -> (out U64):
// ERROR: 'a' is an argument, which is READ-ONLY.
// You cannot modify an input parameter.
a += 1
// This is valid. We read 'a' and 'b', and write to 'out'.
out = a + b
// ERROR: 'out' is a return value, which is WRITE-ONLY.
// This line tries to read 'out' (out = out + 1), which is not allowed.
// You can only assign to 'out' (e.g., out = a + b + 1).
out += 1
When we're calling a function from another package, the function name has to be prepended with scope, e.g. math::four() in the secton on packages.
Deploy and Enlist
Every endpoint can get invoked, but two special qualifiers are available for deploying and enlisting. deploy endpoint is used when the logic is first deployed on the blockchain and it initializes the logic's state. If there's no state logic in the logic, deploy endpoints are not needed.
When we have state actor we can have enlist endpoints that initialize actor's state.
In modules having a logic state, it is necessary to have at least one deploy endpoint to initialize this state (there can be multiple if there are different ways to deploy a logic). Modules with actor states need at least one enlist endpoint, but if a module has both logic and actor states, only deploy is mandatory.
deploy endpoint can be called only once at deployment of the logic, while there's no limit for other endpoints (including enlist).
coco Client
state logic:
supply U64
state actor:
balance U64
endpoint deploy SeedSupply(): // initialize state when deploying logic
mutate 100 -> Client.Logic.supply
endpoint enlist Alms():
mutate 2 -> Client.Sender.balance // some small initial balance
// adds 1 to the Sender's balance, if there's still some supply
endpoint dynamic GimmeGimme():
mutate sup <- Client.Logic.supply:
sup -= 1 // if supply is gone, this throws an error
mutate cs <- Client.Sender.balance:
cs += 1