diff --git a/project.ts b/project.ts index 39aaf9dc..14507895 100644 --- a/project.ts +++ b/project.ts @@ -87,6 +87,13 @@ const project: CosmosProject = { type: "state_change", }, }, + { + handler: "handleTransferEvent", + kind: CosmosHandlerKind.Event, + filter: { + type: "transfer", + }, + }, ], }, }, diff --git a/schema.graphql b/schema.graphql index e0dd4bca..58b6313f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -263,3 +263,10 @@ type ReserveMetrics @entity { totalFeeMinted: BigInt! allocations: [ReserveAllocationMetrics] @derivedFrom(field: "reserveMetrics") } + +type Balances @entity { + id: ID! + address: String @index + balance: BigInt + denom: String @index +} \ No newline at end of file diff --git a/src/mappings/constants.ts b/src/mappings/constants.ts index 465b9302..6fab244b 100644 --- a/src/mappings/constants.ts +++ b/src/mappings/constants.ts @@ -24,6 +24,12 @@ export const VAULT_STATES = { LIQUIDATED: "liquidated" } +export const TRANSACTION_FIELDS = { + RECIPIENT: 'recipient', + SENDER: 'sender', + AMOUNT: 'amount' +} + export const VALUE_KEY = b64encode("value"); export const STORE_KEY = b64encode("store"); export const VSTORAGE_VALUE = b64encode("vstorage"); diff --git a/src/mappings/events/balances.ts b/src/mappings/events/balances.ts new file mode 100644 index 00000000..75b223e6 --- /dev/null +++ b/src/mappings/events/balances.ts @@ -0,0 +1,98 @@ +import { Balances } from '../../types'; +import { b64decode } from '../utils'; +import { CosmosEvent } from '@subql/types-cosmos'; + +interface Attribute { + key: string; + value: string; +} + +export interface DecodedEvent { + type: string; + attributes: Attribute[]; +} +export const balancesEventKit = () => { + function decodeEvent(cosmosEvent: CosmosEvent): DecodedEvent { + const { event } = cosmosEvent; + + const decodedData: DecodedEvent = { + type: event.type, + attributes: [], + }; + + event.attributes.forEach((attribute) => { + const decodedKey = b64decode(attribute.key); + const decodedValue = b64decode(attribute.value); + + decodedData.attributes.push({ + key: decodedKey, + value: decodedValue, + }); + }); + + return decodedData; + } + + async function addressExists(address: string): Promise { + const balance = await Balances.getByAddress(address); + if (!balance) { + return false; + } + return true; + } + + async function createBalancesEntry(address: string) { + const newBalance = new Balances(address); + newBalance.address = address; + newBalance.balance = BigInt(0); + newBalance.denom = ''; + + await newBalance.save(); + + logger.info(`Created new entry for address: ${address}`); + } + + async function updateBalances( + senderAddress: string, + recipientAddress: string, + amount: bigint + ): Promise { + const senderBalances = await Balances.getByAddress(senderAddress); + const recipientBalances = await Balances.getByAddress(recipientAddress); + + if (!senderBalances || senderBalances.length === 0) { + logger.error(`Sender balance not found for address: ${senderAddress}`); + return; + } + + if (!recipientBalances || recipientBalances.length === 0) { + logger.error( + `Recipient balance not found for address: ${recipientAddress}` + ); + return; + } + + const senderBalance = senderBalances[0]; + const recipientBalance = recipientBalances[0]; + + const senderCurrentBalance = senderBalance.balance ?? BigInt(0); + const recipientCurrentBalance = recipientBalance.balance ?? BigInt(0); + + senderBalance.balance = senderCurrentBalance - amount; + recipientBalance.balance = recipientCurrentBalance + amount; + + await senderBalance.save(); + await recipientBalance.save(); + + logger.info( + `Updated balances: Sender ${senderAddress} balance: ${senderBalance.balance}, Recipient ${recipientAddress} balance: ${recipientBalance.balance}` + ); + } + + return { + decodeEvent, + addressExists, + createBalancesEntry, + updateBalances, + }; +}; diff --git a/src/mappings/mappingHandlers.ts b/src/mappings/mappingHandlers.ts index 891acd38..47604e6b 100644 --- a/src/mappings/mappingHandlers.ts +++ b/src/mappings/mappingHandlers.ts @@ -37,12 +37,14 @@ import { STORE_NAME_KEY, SUBKEY_KEY, UNPROVED_VALUE_KEY, + TRANSACTION_FIELDS } from "./constants"; import { psmEventKit } from "./events/psm"; import { boardAuxEventKit } from "./events/boardAux"; import { priceFeedEventKit } from "./events/priceFeed"; import { vaultsEventKit } from "./events/vaults"; import { reservesEventKit } from "./events/reserves"; +import { DecodedEvent, balancesEventKit } from "./events/balances"; // @ts-ignore BigInt.prototype.toJSON = function () { @@ -163,3 +165,43 @@ export async function handleStateChangeEvent(cosmosEvent: CosmosEvent): Promise< await Promise.allSettled(recordSaves); } + +export async function handleTransferEvent( + cosmosEvent: CosmosEvent +): Promise { + const { event, block } = cosmosEvent; + + if (event.type != EVENT_TYPES.TRANSFER) { + logger.warn('Not valid transfer event.'); + return; + } + + const balancesKit = balancesEventKit(); + const decodedData: DecodedEvent = balancesKit.decodeEvent(cosmosEvent); + + const recipientAddress = + decodedData.attributes.find( + (attr) => attr.key === TRANSACTION_FIELDS.RECIPIENT + )?.value || ''; + + if (recipientAddress && (await balancesKit.addressExists(recipientAddress))) { + await balancesKit.createBalancesEntry(recipientAddress); + } + + const senderAddress = + decodedData.attributes.find( + (attr) => attr.key === TRANSACTION_FIELDS.SENDER + )?.value || ''; + + if (senderAddress && (await balancesKit.addressExists(senderAddress))) { + await balancesKit.createBalancesEntry(senderAddress); + } + + const amount = BigInt( + decodedData.attributes + .find((attr) => attr.key === TRANSACTION_FIELDS.AMOUNT) + ?.value.slice(0, -4) || 0 + ); + + balancesKit.updateBalances(senderAddress, recipientAddress, amount); +}