Skip to main content

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 public or external functions.
#[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 view functions, 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

FormMeaning
#[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:

GroupRoleAttribute sugarDetails
mainordinary method (default)
upc::prepare, upc::request, upc::responseUPC handler phasesupc(prepare), upc(request), upc(response)Universal Procedure Call
oracle::certified::<topic>certified network updateOracles and Certified Calls
oracle::single_phase::<topic>single-phase validateOracles and Certified Calls
oracle::two_phase::<topic>two-phase propose / aggregateOracles and Certified Calls

Addressing across groups differs by mechanism:

  • call! targets methods by name in the main group only — the inter-Lyquid call host API takes no group argument.
  • trigger! can target a specific group with its trigger!((a::b) method(...), mode) form.
  • export = eth records the method's group in its export metadata, so exported methods remain addressable through the Ethereum ABI even outside main (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 where clause
  • the first parameter must be a named context reference, such as ctx: &mut _ or ctx: &_
  • 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 self are 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 categoryContextState access
networkctx: &mut _mutable ctx.network
networkctx: &_immutable ctx.network
instancectx: &mut _immutable ctx.network, mutable ctx.instance
instancectx: &_immutable ctx.network, immutable ctx.instance
certified networkctx: &mut _mutable ctx.network plus ctx.cert
UPC preparectx: &_immutable ctx.network plus ctx.cache
UPC requestctx: &mut _immutable ctx.network, mutable ctx.instance, ctx.from, ctx.id
UPC responsectx: &_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:

FieldMeaning
ctx.lyquid_idCurrent Lyquid ID
ctx.originOriginal external origin
ctx.callerDirect caller
ctx.inputRaw encoded input bytes
ctx.networkGenerated network state accessor
ctx.instanceGenerated instance state accessor, instance contexts only
ctx.node_idCurrent 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:

VariantTypical meaning
LyquidError::LyquorInputHost or external input could not be decoded
LyquidError::LyquidInputGuest supplied invalid input to a host-facing operation
LyquidError::LyquorOutputHost output could not be decoded
LyquidError::LyquidOutputGuest returned invalid output
LyquidError::LyquorRuntime(String)Host/runtime-facing failure
LyquidError::LyquidRuntime(String)Application/runtime failure in Lyquid code
LyquidError::InputCertCertified 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! or eprintln!
  • 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 after ms milliseconds; 0 is immediate
  • TriggerMode::Recurrent(ms): run repeatedly at the given interval
  • TriggerMode::Commit: run once after the current slot commits successfully
  • TriggerMode::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! and print! for Lyquid console output
  • eprintln! and eprint! for error console output
  • log!(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.