Sharing Storage Between Web3 Functions Tasks

Gelato Team

Aug 3, 2023

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 they don't retain memory of past executions. However, sometimes state persistence between runs is needed. This is where a simple key/value store comes into play, accessible from the Web3 Function context.

Example:

  • Retrieve Previous State: The previous block number is retrieved using await storage.get(). Stored values are strings, so they must be parsed into numbers.

  • 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
  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) {
    await storage.set("lastBlockNumber", newBlock.toString());
  }

  return {
    canExec: false,
    message: `Updated block number: ${newBlock.toString()}`,
  };
});

Create a storage.json file in your Web3 Function directory to populate testing 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.

Lottery Contract

  • addName(): Users can request to take part. Emits an AddParticipant event.

  • updateWinner(): Called only by the Web3 Function to update the winner.

  • getLastWinner(): Returns the current winner.

Web3 Functions

  • readlogs: Fetches AddParticipant events and updates the Polybase database.

  • lottery: Queries the Participants collection in Polybase, randomly picks a winner, and updates the Lottery contract.

Polybase Database

Polybase is a decentralized database using zero-knowledge proofs. Only the Web3 Functions can access it using securely stored public/private keys.

Code

Lottery.sol

// 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, "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())

@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 reads AddParticipant events, processes new participants, and updates Polybase.

const currentBlock = await provider.getBlockNumber();
const lastBlockStr = await storage.get("lastBlockNumber");
let lastBlock = lastBlockStr ? parseInt(lastBlockStr) : userArgs.genesisBlock;

const topics = [lottery.interface.getEventTopic("AddParticipant")];
const newParticipants = [];

while (lastBlock < currentBlock) {
  const fromBlock = lastBlock;
  const toBlock = Math.min(fromBlock + MAX_RANGE, currentBlock);
  const eventFilter = { address: lottery.address, topics, fromBlock, toBlock };
  const transferLogs = await provider.getLogs(eventFilter);
  for (const log of transferLogs) {
    const parsed = lottery.interface.parseLog(log);
    const [participant, name] = parsed.args;
    newParticipants.push({ name, participant });
  }
  lastBlock = toBlock;
}
await storage.set("lastBlockNumber", lastBlock.toString());

Database Interaction

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 fetches Polybase participants, picks a random winner, and calls the Lottery contract to update the winner.

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;

return {
  canExec: true,
  callData: [
    {
      to: lotteryAddress,
      data: lottery.interface.encodeFunctionData("updateWinner", [winner]),
    },
  ],
};

Further Reading

Conclusion

Gelato Web3 Functions, through Polybase, enable shared storage between tasks. The readlogs function updates participants, the lottery function selects the winner, and Polybase provides the decentralized storage layer ensuring consistency across 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 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: Provide users with gasless transactions via a simple API.

  • Gasless Wallet: Enable world-class UX by combining Gelato Relay with Safe's Smart Contract Wallet, supporting Account Abstraction.

Witness the ongoing journey towards a decentralized future led by Gelato!

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.