Comment on page
🧞♂
Custom Scripting
Custom processor scripts run in a Javascript (Node.js) runtime.
To facilitate writing scripts faster and easier several global variables are available in your custom scripts. It is optional to use them.
Provides interface to a managed database for storing and retrieving entities or running SQL queries from within your custom processors.
Creates or updates an entity based on Type and Id. This function handles re-orgs automatically.
Remember that
database
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.Example usage:
await database.upsert({
// Entity type is used to group entities together,
// also will be used when querying your data.
entityType: 'UserPosition',
// Unique ID for this entity.
//
// Soem useful tips:
// - chainId makes sure if potentially same tx has happened on different chains it will be stored separately.
// - hash and logIndex make sure this event is stored uniquely.
// - hash also makes sure with potential reorgs we don't store same event twice.
// - localIndex also makes sure if such event has happened multiple times in same tx it will be stored separately.
entityId: `${event.chainId}-${event.txHash}-${event.log.localIndex}`,
// Horizon helps with handling re-orgs and chronological ordering of entities.
horizon: event.horizon,
// You can store any data you want.
// Must not include "entityId" field as it's already defined above.
data: {
chainId: event.chainId,
contractAddress: event.log.address,
blockTimestamp: event.blockTimestamp,
txHash: event.txhash,
// e.g. You can save all event args as-is:
...event.parsed.args,
},
})
Gets a single entity based on Type and Id.
Remember that
database
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.const pairId =
`${event.parsed.args.token0.toString()}#${event.parsed.args.token1.toString()}`;
// You can fetch single entity by its ID and Type
const pair = await database.get({
entityType: 'Pair',
// A best practice is to use lowercase for IDs (especially for addresses)
entityId: pairId.toLowerCase(),
// If you don't need a fresh data (e.g. just to check if entity exists)
// you can use cache (default: false)
cache: true,
});
// One usage of database.get() is to populate a complex entity
// if it does not exist yet.
if (!pair) {
const contract = await blockchain.getContract(
transaction.chainId,
event.log.address,
['function asset() external view returns (address)'],
);
await database.upsert({
entityType: 'Pair',
entityId: pairId,
horizon: event.horizon,
data: {
chainId: event.chainId,
token0: event.parsed.args.token0,
token1: event.parsed.args.token1,
underlyingAsset: await contract.asset(),
},
});
}
Executes an arbitrary SQL query to fetch one or my entities, or any sort of aggregation on top of your existing data.
Remember that
database
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.const ownerAddress = '0x4ba15d5f02394b774731e2be83213028303cce75';
const positions = await database.query(`
SELECT
chainId,
contractAddress,
MAX(horizon) as highestHorizon,
SUM(IF(entityType = 'Deposit', 1, 0)) as totalDeposits,
SUM(IF(entityType = 'Withdraw', 1, 0)) as totalWithdraws,
TRY_CAST(
(
SUM(IF(entityType = 'Deposit', TRY_CAST(shares as u256), TRY_CAST(0 as u256))) -
SUM(IF(entityType = 'Withdraw', TRY_CAST(shares as u256), TRY_CAST(0 as u256)))
) as string
) as totalShares
FROM
entities
WHERE
namespace = 'fuji-finance' AND
(entityType = 'Deposit' OR entityType = 'Withdraw') AND
owner = '${ownerAddress}'
GROUP BY
chainId, contractAddress
ORDER BY totalShares DESC
LIMIT 1000
`);
console.log(`My total deposits list: `, positions.map(p => p.totalDeposits));
Hint: In this example both Deposit and Withdraw entities are queried and aggregated. Also
TRY_CAST(fieldName as u256)
is making sure big numbers (18 decimals) are treated as numbers (not strings).Provides interface to get ethers.js instances easily. You can still use
ethers.*
variable directly if you need to.Returns an ethers.js Provider instance, that uses all your RPC sources behind the scenes and failovers if one of them is not working.
Remember that
blockchain
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.Example usage:
// Note chainId must be defined in your manifest.yml
const provider = await blockchain.getProvider(chainId);
console.log(`My example block number`, {
blockNumber: await provider.getBlockNumber(),
});
Returns a read-only ethers.js Contract instance, that uses all your RPC sources behind the scenes and failovers if one of them is not working.
Remember that
blockchain
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.Example usage:
const contract = await blockchain.getContract(
transaction.chainId,
event.log.address, // This is the contract that emitted current event log
[
// A good practice is to put the ABI methods you use as
// inline, unless you're using many methods.
'function balanceOf(address) view returns (uint256)',
'function claimableBalanceOf(address) view returns (uint256)',
'function claimedBalanceOf(address) view returns (uint256)',
'function lockedBalanceOf(address) view returns (uint256)',
],
);
// Running all at once causes requests to be batched together towards RPC endpoints
const [balance, claimable, claimed, locked] = await Promise.allSettled([
contract.balanceOf(userAddress),
contract.claimedBalanceOf(userAddress),
contract.claimableBalanceOf(userAddress),
contract.lockedBalanceOf(userAddress),
]);
console.log(`My example data`, {
balance: balance.value,
claimable: claimable.value,
claimed: claimed.value,
locked: locked.value,
});
Provides interface to a managed Redis instance that must be used as temporary storage only for the purpose of performance gains, or idempotency facility. Remember that all keys will eventually be evicted based on LRU algorithm.
Get a specific cached item. If item was originally an object (or any other type) it'll be return as fully parsed (as original type).
Remember that
cache
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.Example usage:
const idempotencyKey =
`discord-notify:${chainId}-${transaction?.forkIndex}-${contractAddress}-${txHash}-${event?.log?.logIndex}`.toLowerCase();
const alreadySent = await cache.get(idempotencyKey);
if (!alreadySent) {
await integrations.discord.sendMessage({
guildId: process.env.DISCORD_GUILD_ID,
channelName: process.env.DISCORD_CHANNEL_NAME,
embeds: [embed],
});
await cache.set(idempotencyKey, true, { EX: 60 * 60 * 24 * 5 });
}
Sets a cache entry, the value will be stored as JSON.stringify and will be parsed back on retrieval.
Remember that
cache
is always available as a global variable and you can access it anywhere in your custom script, no need to import or inject it.options.EX
key expiry duration in seconds.
// Cache some variable for up to 5 days
const someVariable = { abc: 123 };
await cache.set('my-unique-key', someVariable, { EX: 60 * 60 * 24 * 5 });
You don't need an API Key from these providers, but if you need you can set your own API Key, for example if you have a bigger plan with these providers.
Already built integration with Discord APIs to send any arbitrary message to any channel from right your custom processors.
Any packages defined in
"dependencies"
field in your package.json will be automatically installed. Make sure that your lock files (e.g. pnpm-lock.yaml) do exist in your repo (same place as manifest.yml).Last modified 3mo ago