Skip to content

Feature request: Condition based notification/callback system #2049

@Bobface

Description

@Bobface

There can be certain fields in a Subgraph which might seem simple to implement at first but will turn out to require a block handler which runs every single block. Most of the time this block handler will check for a condition and return if nothing is to be done, wasting resources and especially sync time.

Problem

Consider the following example contract:

pragma solidity >=0.7.0;

contract ETHLock {
    
    uint constant LOCK_DURATION = 10000;
    mapping(address => mapping(uint => uint)) lockedAmounts;
    
    event Deposited(address user, uint amount, uint lockedUntil);
    event Withdrawn(address user, uint amount, uint lockedUntil);
    
    function deposit() external payable returns (uint) {
        uint lockedUntil = block.timestamp + LOCK_DURATION;
        lockedAmounts[msg.sender][lockedUntil] += msg.value;
        
        emit Deposited(msg.sender, msg.value, lockedUntil);
        return lockedUntil;
    }
    
    function withdraw(uint lockedUntil) external {
        require(block.timestamp > lockedUntil, "too early");

        uint amount = lockedAmounts[msg.sender][lockedUntil];
        lockedAmounts[msg.sender][lockedUntil] = 0;
        
        emit Withdrawn(msg.sender, amount, lockedUntil);
        msg.sender.transfer(amount);
    }

}

This contract accepts ETH and locks them for LOCK_DURATION seconds. After this duration has passed the depositor has the option to withdraw the ETH again.

Let's say the subgraph should offer the following entity:

type UserLockedStats @entity {
  "id = user address"
  id: ID!

  "The total amount of ETH this user has deposited to this contract"
  totalDeposited: BigInt!

  "The total amount of ETH this user has withdrawn from this contract"
  totalWithdrawn: BigInt!

  "The amount of ETH withdrawable at the current moment"
  withdrawable: BigInt!
}

totalDeposited and totalWithdrawn can be easily calculated from the Deposited and Withdrawn events.

withdrawable however is another story as it depends on the current blockchain time and might change with every block.

Current solution

We need to keep a list of all ETH that will be unlocked in the future. For every Deposited event we create a FutureUnlock entity which we append to the AllFutureUnlocks singleton's futureUnlocks list which is sorted by lockedUntil ascending.

type FutureUnlock @entity {
	"id = some unique id"
	id: ID!

	"The user's address"
	user: Bytes!

	"The timestamp when this amount will be unlocked"
	lockedUntil: BigDecimal!

	"The amount"
	amount: BigInt!
}

type AllFutureUnlocks @entity {
	"id = some id"
	id: ID!

	"List of all FutureUnlocks"
	futureUnlocks: [FutureUnlock!]!
}

Then, we need a block handler which runs on every block. The block handler will iterate over AllFutureUnlocks.futureUnlocks. For every element in the list the block handler will check if the current blockchain time exceeds lockedUntil. If that is the case this FutureUnlock is removed from the AllFutureUnlocks.futureUnlocks list and the corresponding UserLockedStats.withdrawable is updated. Since AllFutureUnlocks.futureUnlocks is ordered by lockedUntil ascending, we can exit the loop early once we hit the first element where the current blockchain time is lower than lockedUntil.

The problem is that this block handler will most of the time do nothing and exit on the first element. This wastes resources and increases the sync time by an extreme amount.

A possible solution

When a new Deposited event is handled we already know the time when this amount will be unlocked. If there was a notification system which allows a handler to request a callback on a later point in time based on a condition (timestamp exceeded, block number exceeded, ...) we could avoid the block handler:

export function handleDepositedEvent(event: Deposited): void { 
	/*
		...
		Update the UserLockedStats entity
		...
	*/

	// Set a callback to `unlockCallback` with parameters `event.params` on condition timestamp > event.params.lockedUntil
	NotificationSystem.setCallback(unlockCallback, event.params, { timestamp_gt: event.params.lockedUntil})
}

function unlockCallback(params): void {
	// Update UserLockedStats.withdrawable
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions