Functions
Functions in Lyquid are declared with attribute macros under method and must be defined as
top-level (module) functions. They split into two categories, each mapping naturally to one of the
two state categories:
#[method::network]
fn update(ctx: &mut _, value: U256) -> LyquidResult<bool> {
Ok(true)
}
#[method::instance]
fn query(ctx: &_) -> LyquidResult<String> {
Ok("ok".into())
}
Network Functions (Lyquid-level Execution)
- Every node hosting the same Lyquid executes the identical, deterministic sequence of calls to network functions.
- Can only read and write network state.
- Similar to Solidity's
publicorexternalfunctions.
#[method::network]
fn transfer(ctx: &mut _, to: Address, amount: U256) -> LyquidResult<bool> {
let from = ctx.caller.clone();
transfer(&mut ctx.network, from, to, amount)?;
Ok(true)
}
#[method::network]
fn approve(ctx: &mut _, spender: Address, value: U256) -> LyquidResult<bool> {
approve(&mut ctx.network, ctx.caller, spender, value, true)?;
Ok(true)
}
Calling Other Lyquids
Network functions can invoke another Lyquid's network functions with the
call! macro, giving you the same
on-chain composability as contracts calling contracts on Ethereum. Here is a real example from the
BasicSwap example, which moves tokens by calling an ERC20 Lyquid:
fn _safe_transfer(token: LyquidID, to: Address, amount: U256) -> LyquidResult<()> {
if !call!((token).transfer(to: Address = to, amount: U256 = amount) -> (success: LyquidResult<bool>)).success? {
return Err(LyquidError::LyquidRuntime("TRANSFER_FAILED".into()));
}
Ok(())
}
The syntax mirrors a function declaration: (callee) is any expression yielding a
LyquidID, each parameter is written as name: Type = value, and each named output becomes a
field on the returned value. call! has strict atomic semantics: if the inter-Lyquid call fails,
the whole sequenced execution unwinds together, just like a reverted nested call on Ethereum.
It is only usable from network functions; off-chain (instance) code coordinates with other
Lyquids and nodes via UPC instead.
Instance Functions (Node-level Execution)
If you're coming from Solidity, think of instance functions as supercharged view functions with much more power.
- Like
viewfunctions, they can read network state, but not modify it. - Unlike Solidity, they can also read/write instance state and handle events that may differ from node to node, such as network messages.
In Lyquid, instance functions are a first-class concept that unlock entirely new design patterns:
- Resilient API request and response (via UPC)
- Heavy-lifting computation
- Local caching and performance metrics
#[method::instance]
fn balance_of(ctx: &_, account: Address) -> LyquidResult<U256> {
// Can function like a regular `view` function as in Solidity, &ctx promises not to modify any state.
Ok(get_balance(&ctx.network, &account).clone())
}
#[method::instance]
fn party_sign(ctx: &_, tx: Transaction) -> LyquidResult<Option<Signature>> {
// Instance state variables are lock-guarded; use .read()/.write() to access them.
Ok(if validate(&tx) {
ctx.instance.multi_sig_key.read().as_ref().map(|key| key.sign(tx.digest()))
} else {
None
})
}
#[method::instance]
fn record_transaction(ctx: &mut _, tx: Transaction) -> LyquidResult<bool> {
ctx.instance.tx_history.write().push(tx);
Ok(true)
}
#[method::instance]
fn analyze_trading_pattern(ctx: &_, user: Address) -> LyquidResult<Analysis> {
let history = ctx.instance.tx_history.read();
let user_txs: Vec<_> = history.iter().filter(|tx| tx.user == user).collect();
Ok(analyze_transactions(&user_txs))
}
💡 Unlike Solidity, instance functions can access node-local state and react to off-chain events. You can even define functions that aggregate results across multiple nodes via UPC, enabling powerful new architectures, like customized oracle and bridging, decentralized order matching, multi-party computation, and so on.
Method Attribute Forms
| Form | Meaning |
|---|---|
#[method::network] | Network method in the default main group |
#[method::network(group = foo::bar)] | Network method in group foo::bar |
#[method::network(export = eth)] | Network method exported through Ethereum ABI metadata |
#[method::network(group = foo, export = eth)] | Grouped network method with ETH export |
#[method::instance] | Instance method in the default main group |
#[method::instance(group = foo::bar)] | Instance method in group foo::bar |
#[method::instance(export = eth)] | Instance method exported through Ethereum ABI metadata |
#[method::instance(upc(prepare))] | UPC callee-selection handler |
#[method::instance(upc(request))] | UPC request handler |
#[method::instance(upc(response))] | UPC response aggregator |
The only currently supported export kind is eth. It may be written as export = eth or
export = "eth".
Method Groups
Every method belongs to a group, a path-style namespace written with ::. The default group is
main, so #[method::network] and #[method::network(group = main)] are equivalent. Specify
another group with group = a::b::c.
#[method::network(group = admin)]
fn pause(ctx: &mut _) -> LyquidResult<bool> {
Ok(true)
}
A method name must be unique within its group, but the same name may appear in several groups. That is how one logical endpoint is described by multiple functions with different roles — UPC phases for a single call site, or oracle handlers for a single topic.
The group prefix can carry special semantics: it changes when the function runs and what its
generated ctx exposes. Reserved prefixes:
| Group | Role | Attribute sugar | Details |
|---|---|---|---|
main | ordinary method (default) | — | — |
upc::prepare, upc::request, upc::response | UPC handler phases | upc(prepare), upc(request), upc(response) | Universal Procedure Call |
oracle::certified::<topic> | certified network update | — | Oracles and Certified Calls |
oracle::single_phase::<topic> | single-phase validate | — | Oracles and Certified Calls |
oracle::two_phase::<topic> | two-phase propose / aggregate | — | Oracles and Certified Calls |
Addressing across groups differs by mechanism:
call!targets methods by name in themaingroup only — the inter-Lyquid call host API takes no group argument.trigger!can target a specific group with itstrigger!((a::b) method(...), mode)form.export = ethrecords the method's group in its export metadata, so exported methods remain addressable through the Ethereum ABI even outsidemain(certified oracle handlers are commonly exported this way).
Signature Rules
Ordinary network and instance methods must follow this shape:
#[method::network]
fn method_name(ctx: &mut _, arg: Type) -> LyquidResult<ReturnType> {
Ok(value)
}
Rules enforced by the proc macro:
- the function must not be
async - the function must not be
const - the function must not be
extern - the function must not be variadic
- the function must not have generic parameters or a
whereclause - the first parameter must be a named context reference, such as
ctx: &mut _orctx: &_ - every later parameter must be a named identifier, such as
amount: U256 - ordinary methods must return
LyquidResult<T> - top-level method receivers like
self,&self, and&mut selfare not supported
Constructor
A constructor is optional. If present, it must be a network method named constructor.
#[method::network(export = eth)]
fn constructor(ctx: &mut _, greeting: String) {
*ctx.network.greeting = greeting;
}
Constructor rules:
- it must be named
constructor - it must not return a value
- it may take constructor parameters after the context parameter
- it must not specify
group = ... - it may specify
export = eth - it is invoked atomically once at deployment or code upgrade
Context Mutability
The context reference controls whether the generated context exposes mutable state.
| Method category | Context | State access |
|---|---|---|
network | ctx: &mut _ | mutable ctx.network |
network | ctx: &_ | immutable ctx.network |
instance | ctx: &mut _ | immutable ctx.network, mutable ctx.instance |
instance | ctx: &_ | immutable ctx.network, immutable ctx.instance |
| certified network | ctx: &mut _ | mutable ctx.network plus ctx.cert |
| UPC prepare | ctx: &_ | immutable ctx.network plus ctx.cache |
| UPC request | ctx: &mut _ | immutable ctx.network, mutable ctx.instance, ctx.from, ctx.id |
| UPC response | ctx: &_ | immutable ctx.network, ctx.from, ctx.id, ctx.cache |
Use ctx: &_ for read-only methods. This prevents accidental writes and makes the method's
contract obvious to readers.
Context Fields
Common context fields:
| Field | Meaning |
|---|---|
ctx.lyquid_id | Current Lyquid ID |
ctx.origin | Original external origin |
ctx.caller | Direct caller |
ctx.input | Raw encoded input bytes |
ctx.network | Generated network state accessor |
ctx.instance | Generated instance state accessor, instance contexts only |
ctx.node_id | Current node ID, instance contexts only |
Network contexts do not contain node_id because network execution must not depend on the hosting
node.
Return Values and Errors
Use LyquidResult<T> for ordinary method results.
fn transfer(ctx: &mut _, to: Address, amount: U256) -> LyquidResult<bool> {
if to == Address::ZERO {
return Err(LyquidError::LyquidRuntime("invalid receiver".into()));
}
Ok(true)
}
Prefer explicit Err(LyquidError::...) for expected failures. Avoid panic! for domain errors.
Important error variants:
| Variant | Typical meaning |
|---|---|
LyquidError::LyquorInput | Host or external input could not be decoded |
LyquidError::LyquidInput | Guest supplied invalid input to a host-facing operation |
LyquidError::LyquorOutput | Host output could not be decoded |
LyquidError::LyquidOutput | Guest returned invalid output |
LyquidError::LyquorRuntime(String) | Host/runtime-facing failure |
LyquidError::LyquidRuntime(String) | Application/runtime failure in Lyquid code |
LyquidError::InputCert | Certified call input/certificate did not validate |
LyquidError::OracleError(String) | Oracle flow failure |
Execution Semantics
Network Methods
Network methods are deterministic sequenced methods. They are the place for state transitions that must be globally agreed. Network methods can:
- read/write network state when the context is mutable
- read network state when the context is immutable
- emit Lyte logs with
log! - print to the Lyquid console with
println!oreprintln! - perform inter-Lyquid network calls with
call! - schedule commit-time triggers with
trigger!(..., TriggerMode::Commit)
Network methods must not depend on nondeterministic node-local information.
Instance Methods
Instance methods are node-local methods. They can handle external queries, cache local data, contact external services through host APIs, participate in UPC, and submit calls back to the sequencer. Instance methods can:
- read network state
- read/write instance state when mutable
- perform UPC with
upc! - use host APIs such as signing, random bytes, system time, and HTTP request where appropriate (see Host APIs)
- submit sequenced calls with
submit_certified_call!
Instance methods cannot directly mutate network state.
Inter-Lyquid Network Calls
Use call! from network methods for atomic inter-Lyquid calls.
let (ok,) = call!(
(other_lyquid).transfer(
to: Address = recipient,
amount: U256 = amount
) -> (ok: bool)
);
The macro encodes named inputs and decodes named outputs. If the host call fails or the output cannot be decoded, the current sequenced execution aborts atomically.
Triggers
Use trigger! to invoke an instance function with a trigger mode.
trigger!(refresh_cache(symbol: String = symbol), TriggerMode::Commit);
Available trigger modes are:
TriggerMode::Once(ms): run once aftermsmilliseconds;0is immediateTriggerMode::Recurrent(ms): run repeatedly at the given intervalTriggerMode::Commit: run once after the current slot commits successfullyTriggerMode::Stop: remove the trigger from the registry
TriggerMode::Commit is only valid from network functions and runs after slot commit.
Logs and Console Output
Use:
println!andprint!for Lyquid console outputeprintln!andeprint!for error console outputlog!(tag, value)for network logs
log! is intended for network functions and is similar in spirit to an EVM event.
See the method Rust docs for full syntax and
advanced UPC forms.