✍️ Writing the Code
If you just want to try out the code immediately, jump to Build and Deploy.
Defining State
Lyquid explicitly separates on-chain ("network") and off-chain ("instance") state.
lyquid::state! {
network greeting: network::String = network::new_string();
network greet_count: u64 = 0;
instance per_user_count: instance::HashMap<Address, u64> = instance::new_hashmap();
}
network greetingandgreet_countclosely match Solidity's public storage variables.instance per_user_countintroduces a completely new capability: node-local persistent state, which Solidity does not support.
A Simple, Familiar Constructor
Just like Solidity, our Lyquid constructor initializes global state:
constructor(&mut ctx, greeting: String) {
*ctx.network.greeting = greeting.into();
}
This closely mirrors Solidity’s constructor:
constructor(string memory _greeting) {
greeting = _greeting;
}
The .into() method converts the standard String into a network::String, clearly indicates
allocation into persistent on-chain storage, while remaining simple and ergonomic.
Updating the Greeting (set_greeting)
Next, we implement a simple function to update our greeting message:
network fn set_greeting(&mut ctx, greeting: String) -> LyquidResult<bool> {
*ctx.network.greeting = greeting.into();
Ok(true)
}
Solidity equivalent:
function setGreeting(string calldata _greeting) external {
greeting = _greeting;
}
Both versions are concise. Lyquid’s explicit state model clearly indicates that this function
modifies global on-chain state (network), enhancing clarity and correctness.
Tracking Greetings (greet)
Every greeting increments a global counter:
network fn greet(&mut ctx) -> LyquidResult<bool> {
*ctx.network.greet_count += 1;
Ok(true)
}
Solidity equivalent:
function greet() external {
greetCount += 1;
}
Again, the logic aligns directly, but Lyquid explicitly scopes this change to global state, reinforcing clear ownership and intention.
Efficiently Formatting Messages (get_greeting_message)
The Lyquid version simplifies formatted message generation:
network fn get_greeting_message(&ctx) -> LyquidResult<String> {
Ok(format!("{} I've greeted {} times to on-chain users",
ctx.network.greeting,
ctx.network.greet_count
))
}
Solidity’s version is verbose, requiring external libraries (OpenZeppelin) or manual conversions:
function getGreetingMessage() external view returns (string memory) {
return string(
abi.encodePacked(
greeting,
"I've greeted",
greetCount.toString(),
" times to on-chain users"
)
);
}
Lyquid leverages Rust’s native format! macro, providing concise, efficient, and highly readable code.
Extending Solidity with Off-Chain Power (greet_me)
Now comes the most exciting feature unique to Lyquid. We'd like to also keep some per-user, off-chain state at each node so the computation knows how many times the user calls this method to the node.
instance fn greet_me(&mut ctx) -> LyquidResult<String> {
let mut per_user_count = ctx.instance.per_user_count.write();
let user = per_user_count.entry(ctx.caller).or_default();
*user += 1;
Ok(format!("{} I've greeted {} times to on-chain users, and {} times to you",
ctx.network.greeting, ctx.network.greet_count, *user))
}
Solidity Equivalent: Not Possible!
This function shows Lyquid's off-chain power—node-local, persistent state per user. In this example it's used for personalized interactions (per-user counters) and off-chain analytics. But with off-chain network requests and also UPC, one can build any distributed system, and enjoy the on-chain state management to programmably control the nodes' off-chain behavior seamlessly.
Lyquid keeps the simple, readable style of Solidity, but adds off-chain power, enabling richer and more efficient decentralized services—without extra complexity.
You write less, clearly see state management, and achieve far more.