-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
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
}