🧞‍♂️Custom Scripting

Custom processor scripts run in a Javascript (Node.js) runtime.

Event Processors

Learn about custom event processor scripts structure and handler function here.

Available Globals

To facilitate writing scripts faster and easier several global variables are available in your custom scripts. It is optional to use them.

💫 database to store entities in a managed schemaless SQL database

Provides interface to a managed database for storing and retrieving entities or running SQL queries from within your custom processors.

1️⃣ database.upsert(...)

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: 'Swap',

  // 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.
  // - logIndex 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.logIndex}`,

  // 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.
  chainId: event.chainId,
  contractAddress: event.log.address,
  blockTimestamp: event.blockTimestamp,
  txHash: event.txhash,

  // e.g. You can save all event args as-is:
  // Note that internal storage of Flair is schemaless,
  // so even each entity can have different fields for the same entityType.
  ...event.parsed.args,
})

2️⃣ database.get(...)

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 || !pair?.underlyingAsset) {
  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,

    chainId: event.chainId,
    token0: event.parsed.args.token0,
    token1: event.parsed.args.token1,
    underlyingAsset: await contract.asset(),
  });
}

Need help for your special database use-case?

Ping our engineers to help you on that 🙂

💫 blockchain to interact with any contract on any chain

Provides interface to get ethers.js instances easily. You can still use ethers.* variable directly if you need to.

1️⃣ blockchain.getProvider(chainId)

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(),
});

2️⃣ blockchain.getContract(chainId, address, abi)

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,
});

Need help?

Reach out to our engineers 🙂

💫 cache to keep temporary key-value data for performance reasons

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.

1️⃣ cache.get(key)

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 });
}

2️⃣ cache.set(key, value, options)

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 object is same as @redis/client SetOptions, such as:

  • 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 });

Need help?

Reach out to our engineers 🙂

Integrations

💫 prices for ERC20 tokens in USD for any contract on any chain

Already built integration with CoinGecko and Moralis to get USD-equivalent value of any amount of ERC20 tokens right from your custom processors.

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.

Read the full docs here.

💫 discord to send any custom messages to any Discord channel

Already built integration with Discord APIs to send any arbitrary message to any channel from right your custom processors.

Read the full docs here.

External NPM Libraries

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 updated