476 days ago
Sharing Storage Between Web3 Functions Tasks
What are Gelato's Web3 Functions?
Connecting smart contracts with existing web2 APIs was a big problem in web3 development, creating a significant barrier to the integration of crucial off-chain resources.
Gelato Web3 Functions have emerged as the solution to this challenge. By acting as decentralized cloud functions, they facilitate connections between smart contracts and off-chain data (APIs, subgraphs, etc) without needing separate infrastructure. This unlocks the potential for hybrid applications, allowing on-chain transactions to be executed based on off-chain data, all with simplicity.
State/Storage of Web3 Functions
Gelato's Web3 Functions enable you to perform complex tasks with smart contracts, such as retrieving data from an API, doing computations, and pushing data on-chain when conditions are met.
They are designed to be stateless, meaning that they don't retain memory of past executions. However, there might be cases where you need to manage some state variable between different runs. This is where a simple key/value store comes into play, accessible from the Web3 Function context.
Let’s look at the following example:
-
Retrieve Previous State: The previous block number is retrieved using await storage.get(). Stored values are always strings, so they must be parsed into a number.
-
Check and Update Storage: If the new block number is greater than the last block, the storage is updated with the new value.
import {
Web3Function,
Web3FunctionContext,
} from "@gelatonetwork/web3-functions-sdk";
Web3Function.onRun(async (context: Web3FunctionContext) => {
const { storage, multiChainProvider } = context;
const provider = multiChainProvider.default();
// Use storage to retrieve previous state (stored values are always string)
const lastBlockStr = (await storage.get("lastBlockNumber")) ?? "0";
const lastBlock = parseInt(lastBlockStr);
console.log(`Last block: ${lastBlock}`);
const newBlock = await provider.getBlockNumber();
console.log(`New block: ${newBlock}`);
if (newBlock > lastBlock) {
// Update storage to persist your current state (values must be cast to string)
await storage.set("lastBlockNumber", newBlock.toString());
}
return {
canExec: false,
message: `Updated block number: ${newBlock.toString()}`,
};});
To populate the storage values in your testing, in the same directory as your web3 function, create a file storage.json and fill in the storage values.
{
"lastBlockNumber": "1000"
}
Sharing Storage Between Web3 Functions
Sometimes, there is a need to share state or storage between different Web3 Functions. This can be achieved using Polybase as shared storage. For demonstration purposes, the following structures are created:
Lottery Contract
- addName(): Users can request to take part. This method emits the 'AddParticipant' event.
- updateWinner(): Called only by the Web3 Function that updates the winner.
- getLastWinner(): Returns the current winner.
Web3 Functions
- readlogs: Fetches 'AddParticipant' events and updates the Polybase database if needed.
- lottery: Queries the Participants collection from the Polybase database, randomly picks a winner, and updates the Lottery contract.
Polybase Database
Polybase is a decentralized database that uses zero-knowledge proofs. It creates the database with the Participant collection. To limit access to the Polybase DB, only the web3 function can access it. For this purpose, we generate a public/private key pair and store it securely within web3 Functions secrets.
Code
Smart Contract (Lottery.sol)-
The lottery contract has three public methods:
-
addName(): Anyone can call this method to add their name as a participant in the lottery. The function emits an 'AddParticipant' event with the sender's address and name.
-
updateWinner(): This method is used to declare a winner for the lottery. However, it has a modifier onlyDedicatedMsgSender() that allows only a specific address (which is set during contract deployment) to call this method. This address is the one corresponding to the Gelato Web3 Function, allowing the task to update the winner of the lottery.
-
getLastWinner(): As the name suggests, this method when called, returns the name of the last declared winner of the lottery.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Lottery {
string public winner;
address public immutable dedicatedMsgSender;
event AddParticipant(address participant, string name);
modifier onlyDedicatedMsgSender() {
require(
msg.sender == dedicatedMsgSender,
"LensGelatoGPT.onlyDedicatedMsgSender"
);
_;
}
constructor(address _dedicatedMsgSender) {
dedicatedMsgSender = _dedicatedMsgSender;
}
function addName(string memory _name) external {
emit AddParticipant(msg.sender, _name);
}
function updateWinner(string memory _name) external onlyDedicatedMsgSender {
winner = _name;
}
function getLastWinner() external view returns (string memory) {
return winner;
}
}
Polybase Database (createDb())
This code sets up the Polybase database, a decentralized storage platform. It used to store the lottery participant's data. It creates a new database with a Participants collection and three methods:
- constructor(): Used to create a new record in the Participants collection.
- updateName(): Updates the name of an existing participant.
- del(): Deletes the record of a participant (note: it's not used in this project).
@public
collection Participants {
id: string;
name: string;
constructor (id: string, name: string) {
this.id = id;
this.name = name;
}
updateName(name: string) {
this.name = name;
}
del () {
selfdestruct();
}
}
Gelato Web3 Function (readlogs)
The readlogs Web3 Function tasked with managing participants in a lottery game. It reads the "AddParticipant" events from a specified lottery smart contract, processes new participants, and updates them in a decentralized database, Polybase.
- Initialization and Configuration:
import {
Web3Function,
Web3FunctionContext,
} from "@gelatonetwork/web3-functions-sdk";
import { Contract } from "ethers";
import { lotteryAbi } from "../utils/abis/abis";
import { initDb } from "../utils/db.js";
const MAX_RANGE = 100;
interface record {
name: string;
participant: string;
}
const PRIVATE_KEY = (await secrets.get("PRIVATE_KEY_POLYBASE")) as string;
const PUBLIC_KEY = (await secrets.get("PUBLIC_KEY_POLYBASE")) as string;
const lotteryAddress = userArgs.lotteryAddress;
if (!lotteryAddress) throw new Error("Missing userArgs.lotteryAddress");
const lottery = new Contract(lotteryAddress as string, lotteryAbi, provider);
- Fetching and Processing New Participants:
This part retrieves the current block number from the blockchain and iterates through unprocessed blocks to find specific event logs. It parses these logs to extract participant information, such as name and address, and adds them to a newParticipants array. Finally, it updates the storage with the last processed block number, ensuring that future iterations begin from where it left off.
const currentBlock = await provider.getBlockNumber();
const lastBlockStr = await storage.get("lastBlockNumber");
let lastBlock: number = +(lastBlockStr
? parseInt(lastBlockStr)
: (userArgs.genesisBlock as number));
console.log(`Last processed block: ${lastBlock}, ${lastBlock}`);
// Retrieve new mint event
let nbRequests = 0;
const topics = [lottery.interface.getEventTopic("AddParticipant")];
// Fetch historical events in batch without exceeding runtime limits
const newParticipants: Array<record> = [];
while (lastBlock < currentBlock) {
nbRequests++;
const fromBlock = lastBlock;
const toBlock = Math.min(fromBlock + MAX_RANGE, currentBlock);
try {
console.log(`Fetching log events from blocks ${fromBlock} to ${toBlock}`);
const eventFilter = {
address: lottery.address,
topics,
fromBlock,
toBlock,
};
const transferLogs = await provider.getLogs(eventFilter);
for (const transferLog of transferLogs) {
const transferEvent = lottery.interface.parseLog(transferLog);
const [participant, name] = transferEvent.args;
newParticipants.push({ name, participant });
}
lastBlock = toBlock;
} catch (err) {
return {
canExec: false,
message: `Rpc call failed: ${(err as Error).message}`,
};
} } await storage.set("lastBlockNumber", lastBlock.toString());
- Database Interaction with Polybase:
This part interacts with a Polybase database, checking if a record exists and updating or creating as needed.
const db = await initDb(PRIVATE_KEY, PUBLIC_KEY);
const coll = db.collection("Participants");
for (const participant of newParticipants) {
const record = await coll.record(participant.participant).get();
if (record.exists()) {
await record.call("updateName", [participant.name]);
} else {
await coll.create([participant.participant, participant.name]);
}
}
Gelato Web3 Function (lottery)
The lottery Web3 function works closely with another Web3 function, called "readLogs." Together, they create a comprehensive system to manage a decentralized lottery. The readLogs function continuously monitors the blockchain for new participant entries in the lottery and updates the "Participants" collection within Polybase. Once the participants are updated, the lottery Web3 function fetches this data from Polybase. If there are participants, it randomly selects a winner and prepares a function call to update the winner within the lottery smart contract itself.
Polybase's role is essential here, serving as the decentralized database for storing and retrieving vital information. It ensures that data is consistent and accessible across both Web3 functions, allowing them to work in tandem.
In summary, the readLogs function updates the participants, the lottery Web3 function selects the winner, and Polybase facilitates the shared storage. This integration results in the efficiency and functionality needed to run a lottery system in a decentralized manner, with the winner's information properly recorded on the blockchain.
import {
Web3Function,
Web3FunctionContext,
} from "@gelatonetwork/web3-functions-sdk";
import { Contract } from "ethers";
import { lotteryAbi } from "../utils/abis/abis";
import { initDb } from "../utils/db.js";
Web3Function.onRun(async (context: Web3FunctionContext) => {
const { userArgs, secrets, multiChainProvider } = context;
const provider = multiChainProvider.default();
// User Secrets
const PRIVATE_KEY = (await secrets.get("PRIVATE_KEY_POLYBASE")) as string;
const PUBLIC_KEY = (await secrets.get("PUBLIC_KEY_POLYBASE")) as string;
const lotteryAddress = userArgs.lotteryAddress as string;
if (!lotteryAddress) throw new Error("Missing userArgs.lotteryAddress");
const lottery = new Contract(lotteryAddress as string, lotteryAbi, provider);
const db = await initDb(PRIVATE_KEY, PUBLIC_KEY);
const coll = db.collection("Participants");
let res = await coll.get();
if (res.data.length == 0) {
return { canExec: false, message: `There are no participants yet` };
}
const winnerIndex = Math.floor(Math.random() * res.data.length);
const winner = res.data[winnerIndex].data.name;
console.log(`Winner is ${winner}`);
return {
canExec: true,
callData: [
{
to: lotteryAddress,
data: lottery.interface.encodeFunctionData("updateWinner", [winner]),
},
],
};
});
Further Reading
Conclusion
Gelato Web3 Functions, through the integration of Polybase, enable share storage between the Web3 Functions tasks. The "ReadLogs" function handles participant updates, the "lottery" function manages winner selection, and Polybase provides the necessary decentralized database for shared and consistent data storage across these functions.
About Gelato
Get ready to witness a new era of web3 as Gelato, relied upon by over 400 web3 projects, powers the execution of millions of transactions in DeFi, NFT, and Gaming.
Gelato currently offers four services:
-
Web3 Functions: Connect your smart contracts to off-chain data & computation by running decentralized cloud functions.
-
Automate: Automate your smart contracts by executing transactions automatically in a reliable, developer-friendly & decentralized manner
-
Relay: Give your users access to reliable, robust, and scalable gasless transactions via a simple-to-use API
-
Gasless Wallet: A powerful SDK that enables developers to provide a world-class UX by combining Gelato Relay + Safe's Smart Contract Wallet, enabling Account Abstraction
Witness the ongoing journey towards a decentralized future led by Gelato!