From 80f41c6f96b3532544b82247ec7fcf9160a50762 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Tue, 12 Nov 2024 15:32:52 -0400 Subject: [PATCH 1/8] Add total supply handling. - Introduced a new `Supply` type in `schema.graphql`. - Created `getSupply` function in `src/mappings/bank/supply.ts` to fetch total supply. - Modified `handleGenesis` to store the initial supply from genesis state. - Added `_handleSupply` function in `src/mappings/primitives.ts` to periodically index total supply. - Updated the Genesis interface to include supply data. --- schema.graphql | 6 +++ src/mappings/bank/supply.ts | 36 +++++++++++++++ src/mappings/primitives.ts | 82 +++++++++++++++++++++++++---------- src/mappings/types/genesis.ts | 2 + 4 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 src/mappings/bank/supply.ts diff --git a/schema.graphql b/schema.graphql index da2f70d..ad9a5d6 100644 --- a/schema.graphql +++ b/schema.graphql @@ -120,3 +120,9 @@ type GenesisFile @entity { id: ID! raw: String! } + +type Supply @entity { + # coin denom + id: ID! + amount: BigInt! +} diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts new file mode 100644 index 0000000..285a0c1 --- /dev/null +++ b/src/mappings/bank/supply.ts @@ -0,0 +1,36 @@ +import type { QueryTotalSupplyResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; +import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; +import { stringify } from "../utils"; + +export async function getSupply(): Promise { + let finalSupply: Coin[] = []; + let paginationKey: Uint8Array | undefined; + + try { + // Here we force the use of a private property, breaking typescript limitation, due to the need of call a total supply + // rpc query of cosmosjs that is not exposed on the implemented client by SubQuery team. + // To avoid this, we need to move to implement our own rpc client and also use `unsafe` parameter which I prefer to avoid. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const queryClient = api.forceGetQueryClient(); + + // Initial call to get the first set of results + const initialResponse: QueryTotalSupplyResponse = await queryClient.bank.totalSupply() as unknown as QueryTotalSupplyResponse; + logger.debug(`[handleTotalSupply]: initialResponse=${stringify(initialResponse, undefined, 2)}`); + finalSupply = finalSupply.concat(initialResponse.supply); + paginationKey = initialResponse.pagination?.nextKey; + + // Continue fetching if there is a nextKey + while (paginationKey && paginationKey.length > 0) { + logger.debug(`[handleTotalSupply]: loading more supply pages pagination.nextKey=${JSON.stringify(paginationKey, undefined, 2)}`); + const response = await queryClient.bank.totalSupply(paginationKey); + finalSupply = finalSupply.concat(response.supply); + paginationKey = response.pagination?.nextKey; + } + logger.debug(`[handleTotalSupply]: all_total_supply=${JSON.stringify(finalSupply, undefined, 2)}`); + } catch (error) { + logger.error(`[handleTotalSupply] errored: ${error}`); + } + + return finalSupply; +} diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index 76aa290..2a5c0c8 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -12,57 +12,66 @@ import { isString, } from "lodash"; import { + Balance, Block, Event, EventAttribute, + GenesisBalance, + GenesisFile as GenesisEntity, Message, + NativeBalanceChange, + Supply, Transaction, TxStatus, - NativeBalanceChange, - GenesisBalance, - Balance, - GenesisFile as GenesisEntity, } from "../types"; +import { getSupply } from "./bank/supply"; import { PREFIX } from "./constants"; import type { Genesis } from "./types/genesis"; import { attemptHandling, + getBalanceId, messageId, primitivesFromMsg, primitivesFromTx, stringify, trackUnprocessed, unprocessedEventHandler, - getBalanceId, } from "./utils"; export async function handleGenesis(block: CosmosBlock): Promise { - const genesis: Genesis = require('../../genesis.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const genesis: Genesis = require("../../genesis.json"); // IMPORTANT: Return early if this is not the genesis initial height as this is called for block indexed! if (block.block.header.height !== genesis.initial_height) { - return + return; } logger.info(`[handleGenesis] (block.header.height): indexing genesis block ${block.block.header.height}`); await Promise.all( [ - store.bulkCreate('Account', genesis.app_state.auth.accounts.map(account => { + store.bulkCreate("Account", genesis.app_state.auth.accounts.map(account => { return { id: account.address, chainId: block.block.header.chainId, - } + }; + })), + store.bulkCreate("Supply", genesis.app_state.bank.supply.map(supply => { + return { + id: supply.denom, + amount: BigInt(supply.amount), + }; })), Event.create({ id: "genesis", type: "genesis", blockId: block.block.id, }).save(), - ] - ) + ], + ); - type EntityToSave = Omit; + type EntityToSave = Omit; const nativeBalanceChanges: Array> = []; const genesisBalances: Array> = []; const balances: Array> = []; @@ -77,26 +86,26 @@ export async function handleGenesis(block: CosmosBlock): Promise { const amountByAccountAndDenom: AmountByAccountAndDenom = genesis.app_state.bank.balances.reduce((acc, balance) => { const amountByDenom: Record = balance.coins.reduce((acc, coin) => ({ ...acc, - [coin.denom]: BigInt(acc[coin.denom] || 0) + BigInt(coin.amount), - }), {} as Record) + [coin.denom]: BigInt(acc[coin.denom] || 0) + BigInt(coin.amount), + }), {} as Record); for (const [denom, amount] of Object.entries(amountByDenom)) { - const id = getBalanceId(balance.address, denom) + const id = getBalanceId(balance.address, denom); if (acc[id]) { - acc[id].amount += amount + acc[id].amount += amount; } else { acc[id] = { amount, denom, accountId: balance.address, - } + }; } } - return acc - }, {} as AmountByAccountAndDenom) + return acc; + }, {} as AmountByAccountAndDenom); - for (const [id, {accountId, amount, denom}] of Object.entries(amountByAccountAndDenom)) { + for (const [id, { accountId, amount, denom }] of Object.entries(amountByAccountAndDenom)) { nativeBalanceChanges.push({ id, balanceOffset: amount.valueOf(), @@ -123,9 +132,9 @@ export async function handleGenesis(block: CosmosBlock): Promise { } await Promise.all([ - store.bulkCreate('GenesisBalance', genesisBalances), - store.bulkCreate('NativeBalanceChange', nativeBalanceChanges), - store.bulkCreate('Balance', balances) + store.bulkCreate("GenesisBalance", genesisBalances), + store.bulkCreate("NativeBalanceChange", nativeBalanceChanges), + store.bulkCreate("Balance", balances), ]); await GenesisEntity.create({ @@ -163,6 +172,8 @@ async function _handleBlock(block: CosmosBlock): Promise { }); await blockEntity.save(); + + await _handleSupply(); } async function _handleTransaction(tx: CosmosTransaction): Promise { @@ -274,6 +285,31 @@ async function _handleEvent(event: CosmosEvent): Promise { } } +async function _handleSupply(): Promise { + // we need to check if the amount is different to avoid duplicate records because subquery does not check that + const totalSupply = await getSupply(); + if (totalSupply.length > 0) { + logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); + for (const supply of totalSupply) { + logger.debug(`[handleSupply] (totalSupply.coin: total supply of ${supply.denom}`); + // we need to check if the amount is different to avoid duplicate records because subquery does not check that + const s = await Supply.get(supply.denom); + if (!s) { + await Supply.create({ + id: supply.denom, + amount: BigInt(supply.amount), + }).save(); + continue; + } else if (s.amount.toString() === supply.amount) { + continue; + } + + s.amount = BigInt(supply.amount); + await s.save(); + } + } +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars async function _handleBlockError(err: Error, _: CosmosBlock): Promise { // NB: we won't have persisted any related entities yet. diff --git a/src/mappings/types/genesis.ts b/src/mappings/types/genesis.ts index cc48302..3111fe4 100644 --- a/src/mappings/types/genesis.ts +++ b/src/mappings/types/genesis.ts @@ -1,5 +1,6 @@ import { BaseAccountSDKType } from "../../types/proto-interfaces/cosmos/auth/v1beta1/auth"; import { Balance } from "../../types/proto-interfaces/cosmos/bank/v1beta1/genesis"; +import { Coin } from "../../types/proto-interfaces/cosmos/base/v1beta1/coin"; export interface Genesis { initial_height: number, @@ -9,6 +10,7 @@ export interface Genesis { } bank: { balances: Array + supply: Array }, } } From d6c72fa7db77e255debd7c1d670b868fa8a6cc14 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Tue, 12 Nov 2024 16:47:22 -0400 Subject: [PATCH 2/8] Add Supply entity and update supply handling logic - Introduced `supplies` field in `Block` type to reference `Supply` entities. - Added new fields `denom` and `block` to `Supply` type. - Replaced `getSupply` with `queryTotalSupply` and implement `getSupplyRecord`. - Modified `_handleSupply` to create supply records with `getSupplyRecord` and avoid duplicates. --- schema.graphql | 3 +++ src/mappings/bank/supply.ts | 15 +++++++++++-- src/mappings/primitives.ts | 44 +++++++++++-------------------------- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/schema.graphql b/schema.graphql index ad9a5d6..06bcd87 100644 --- a/schema.graphql +++ b/schema.graphql @@ -23,6 +23,7 @@ type Block @entity { messages: [Message] @derivedFrom(field: "block") events: [Event] @derivedFrom(field: "block") balancesOfAccountByDenom: [Balance] @derivedFrom(field: "lastUpdatedBlock") + supplies: [Supply] @derivedFrom(field: "block") } type Transaction @entity { @@ -124,5 +125,7 @@ type GenesisFile @entity { type Supply @entity { # coin denom id: ID! + denom: String! @index amount: BigInt! + block: Block! } diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts index 285a0c1..ded367f 100644 --- a/src/mappings/bank/supply.ts +++ b/src/mappings/bank/supply.ts @@ -1,8 +1,19 @@ +import type { + Coin, + CosmosBlock, +} from "@subql/types-cosmos"; import type { QueryTotalSupplyResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; -import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; +import { Supply } from "../../types"; import { stringify } from "../utils"; -export async function getSupply(): Promise { +export const getSupplyRecord = (supply: Coin, block: CosmosBlock): Supply => Supply.create({ + id: `${supply.denom}@${block.block.id}`, + denom: supply.denom, + amount: BigInt(supply.amount), + blockId: block.block.id, +}); + +export async function queryTotalSupply(): Promise { let finalSupply: Coin[] = []; let paginationKey: Uint8Array | undefined; diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index 2a5c0c8..6084e6f 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -20,11 +20,13 @@ import { GenesisFile as GenesisEntity, Message, NativeBalanceChange, - Supply, Transaction, TxStatus, } from "../types"; -import { getSupply } from "./bank/supply"; +import { + getSupplyRecord, + queryTotalSupply, +} from "./bank/supply"; import { PREFIX } from "./constants"; import type { Genesis } from "./types/genesis"; import { @@ -57,12 +59,6 @@ export async function handleGenesis(block: CosmosBlock): Promise { chainId: block.block.header.chainId, }; })), - store.bulkCreate("Supply", genesis.app_state.bank.supply.map(supply => { - return { - id: supply.denom, - amount: BigInt(supply.amount), - }; - })), Event.create({ id: "genesis", type: "genesis", @@ -173,7 +169,7 @@ async function _handleBlock(block: CosmosBlock): Promise { await blockEntity.save(); - await _handleSupply(); + await _handleSupply(block); } async function _handleTransaction(tx: CosmosTransaction): Promise { @@ -285,29 +281,15 @@ async function _handleEvent(event: CosmosEvent): Promise { } } -async function _handleSupply(): Promise { - // we need to check if the amount is different to avoid duplicate records because subquery does not check that - const totalSupply = await getSupply(); - if (totalSupply.length > 0) { - logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); - for (const supply of totalSupply) { - logger.debug(`[handleSupply] (totalSupply.coin: total supply of ${supply.denom}`); - // we need to check if the amount is different to avoid duplicate records because subquery does not check that - const s = await Supply.get(supply.denom); - if (!s) { - await Supply.create({ - id: supply.denom, - amount: BigInt(supply.amount), - }).save(); - continue; - } else if (s.amount.toString() === supply.amount) { - continue; - } - - s.amount = BigInt(supply.amount); - await s.save(); - } +async function _handleSupply(block: CosmosBlock): Promise { + const totalSupply = await queryTotalSupply(); + if (totalSupply.length === 0) { + logger.warn(`[handleSupply]: no total supply found`); + return; } + + logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); + await store.bulkCreate("Supply", totalSupply.map(supply => getSupplyRecord(supply, block))); } // eslint-disable-next-line @typescript-eslint/no-unused-vars From 26968ed69414fd49eec7098e46a71bc67a082eb3 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Tue, 12 Nov 2024 17:47:43 -0400 Subject: [PATCH 3/8] Remove unused supply field from Genesis interface --- src/mappings/types/genesis.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mappings/types/genesis.ts b/src/mappings/types/genesis.ts index 3111fe4..cc48302 100644 --- a/src/mappings/types/genesis.ts +++ b/src/mappings/types/genesis.ts @@ -1,6 +1,5 @@ import { BaseAccountSDKType } from "../../types/proto-interfaces/cosmos/auth/v1beta1/auth"; import { Balance } from "../../types/proto-interfaces/cosmos/bank/v1beta1/genesis"; -import { Coin } from "../../types/proto-interfaces/cosmos/base/v1beta1/coin"; export interface Genesis { initial_height: number, @@ -10,7 +9,6 @@ export interface Genesis { } bank: { balances: Array - supply: Array }, } } From 579aef8bca7eca0163ca1962adbc00608753e6ad Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Wed, 13 Nov 2024 10:39:35 -0400 Subject: [PATCH 4/8] Refactor and relocate _handleSupply function - Removed `_handleSupply` from `primitives.ts` and imported it from `supply.ts`. - Reorganized `getSupplyRecord` in `supply.ts` to use `getSupplyId` for ID generation. - Simplified supply concatenation logic in `queryTotalSupply` by using `push` instead of `concat`. - Centralized total supply handling in `supply.ts` for clearer separation of concerns. --- src/mappings/bank/supply.ts | 35 ++++++++++++++++++++++++++--------- src/mappings/primitives.ts | 15 +-------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts index ded367f..5f806c6 100644 --- a/src/mappings/bank/supply.ts +++ b/src/mappings/bank/supply.ts @@ -6,15 +6,21 @@ import type { QueryTotalSupplyResponse } from "cosmjs-types/cosmos/bank/v1beta1/ import { Supply } from "../../types"; import { stringify } from "../utils"; -export const getSupplyRecord = (supply: Coin, block: CosmosBlock): Supply => Supply.create({ - id: `${supply.denom}@${block.block.id}`, - denom: supply.denom, - amount: BigInt(supply.amount), - blockId: block.block.id, -}); +export const getSupplyId = function(supply: Coin, block: CosmosBlock): string { + return `${supply.denom}@${block.block.id}`; +}; + +export const getSupplyRecord = function(supply: Coin, block: CosmosBlock): Supply { + return Supply.create({ + id: getSupplyId(supply, block), + denom: supply.denom, + amount: BigInt(supply.amount), + blockId: block.block.id, + }); +}; export async function queryTotalSupply(): Promise { - let finalSupply: Coin[] = []; + const finalSupply: Coin[] = []; let paginationKey: Uint8Array | undefined; try { @@ -28,14 +34,14 @@ export async function queryTotalSupply(): Promise { // Initial call to get the first set of results const initialResponse: QueryTotalSupplyResponse = await queryClient.bank.totalSupply() as unknown as QueryTotalSupplyResponse; logger.debug(`[handleTotalSupply]: initialResponse=${stringify(initialResponse, undefined, 2)}`); - finalSupply = finalSupply.concat(initialResponse.supply); + finalSupply.push(...initialResponse.supply); paginationKey = initialResponse.pagination?.nextKey; // Continue fetching if there is a nextKey while (paginationKey && paginationKey.length > 0) { logger.debug(`[handleTotalSupply]: loading more supply pages pagination.nextKey=${JSON.stringify(paginationKey, undefined, 2)}`); const response = await queryClient.bank.totalSupply(paginationKey); - finalSupply = finalSupply.concat(response.supply); + finalSupply.push(...response.supply); paginationKey = response.pagination?.nextKey; } logger.debug(`[handleTotalSupply]: all_total_supply=${JSON.stringify(finalSupply, undefined, 2)}`); @@ -45,3 +51,14 @@ export async function queryTotalSupply(): Promise { return finalSupply; } + +export async function _handleSupply(block: CosmosBlock): Promise { + const totalSupply = await queryTotalSupply(); + if (totalSupply.length === 0) { + logger.warn(`[handleSupply]: no total supply found`); + return; + } + + logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); + await store.bulkCreate("Supply", totalSupply.map(supply => getSupplyRecord(supply, block))); +} diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index 6084e6f..ae8a3c5 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -23,10 +23,7 @@ import { Transaction, TxStatus, } from "../types"; -import { - getSupplyRecord, - queryTotalSupply, -} from "./bank/supply"; +import { _handleSupply } from "./bank/supply"; import { PREFIX } from "./constants"; import type { Genesis } from "./types/genesis"; import { @@ -281,16 +278,6 @@ async function _handleEvent(event: CosmosEvent): Promise { } } -async function _handleSupply(block: CosmosBlock): Promise { - const totalSupply = await queryTotalSupply(); - if (totalSupply.length === 0) { - logger.warn(`[handleSupply]: no total supply found`); - return; - } - - logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); - await store.bulkCreate("Supply", totalSupply.map(supply => getSupplyRecord(supply, block))); -} // eslint-disable-next-line @typescript-eslint/no-unused-vars async function _handleBlockError(err: Error, _: CosmosBlock): Promise { From a0d5d446338715259e419ce2f406de7df99745c1 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Wed, 13 Nov 2024 11:03:42 -0400 Subject: [PATCH 5/8] Included a comment about why we handle supply in the current way and what we plan to do on future releases. --- src/mappings/primitives.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index ae8a3c5..d34f2ab 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -166,6 +166,8 @@ async function _handleBlock(block: CosmosBlock): Promise { await blockEntity.save(); + // We need to track the supply on every block, and this is the way we can do with the RPC, but on a future + // it will be replaced by handling the claim/proof settle event. await _handleSupply(block); } From 4ed497508b93618f57cb10ae0ef9b807ed4e2f52 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Wed, 13 Nov 2024 13:15:12 -0400 Subject: [PATCH 6/8] Add support for linking block and supply data - Introduced new handler `handleBlockSupply` to link block with recent supply. - Added `supply` field to `Block` entity in GraphQL schema. - Ensured supply records in genesis block are handled properly. - Modified `_handleSupply` and `_handleBlockSupply` to update and retrieve supply data. - Updated `Genesis` interface to include supply information. --- project.ts | 10 +++++-- schema.graphql | 6 ++-- src/mappings/bank/supply.ts | 52 +++++++++++++++++++++++++++++++---- src/mappings/primitives.ts | 23 ++++++++++++++-- src/mappings/types/genesis.ts | 6 ++-- 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/project.ts b/project.ts index 6e68995..1b5be50 100644 --- a/project.ts +++ b/project.ts @@ -4,8 +4,8 @@ import { CosmosProject, } from "@subql/types-cosmos"; -import * as dotenv from 'dotenv'; -import path from 'path'; +import * as dotenv from "dotenv"; +import path from "path"; const mode = process.env.NODE_ENV || 'production'; @@ -425,6 +425,12 @@ const project: CosmosProject = { type: "coin_received", } }, + // Supply + { + // this will relate the most recent supply with the most recent block on the database + handler: "handleBlockSupply", + kind: CosmosHandlerKind.Block, + }, ], }, }, diff --git a/schema.graphql b/schema.graphql index 06bcd87..ce13ad7 100644 --- a/schema.graphql +++ b/schema.graphql @@ -23,7 +23,7 @@ type Block @entity { messages: [Message] @derivedFrom(field: "block") events: [Event] @derivedFrom(field: "block") balancesOfAccountByDenom: [Balance] @derivedFrom(field: "lastUpdatedBlock") - supplies: [Supply] @derivedFrom(field: "block") + supply: Supply! } type Transaction @entity { @@ -123,9 +123,9 @@ type GenesisFile @entity { } type Supply @entity { - # coin denom id: ID! denom: String! @index amount: BigInt! - block: Block! + lastBlockHeight: BigInt! @index + block: [Block]! @derivedFrom(field: "supply") } diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts index 5f806c6..b3e309a 100644 --- a/src/mappings/bank/supply.ts +++ b/src/mappings/bank/supply.ts @@ -3,7 +3,10 @@ import type { CosmosBlock, } from "@subql/types-cosmos"; import type { QueryTotalSupplyResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; -import { Supply } from "../../types"; +import { + Block, + Supply, +} from "../../types"; import { stringify } from "../utils"; export const getSupplyId = function(supply: Coin, block: CosmosBlock): string { @@ -14,8 +17,8 @@ export const getSupplyRecord = function(supply: Coin, block: CosmosBlock): Suppl return Supply.create({ id: getSupplyId(supply, block), denom: supply.denom, + lastBlockHeight: BigInt(block.block.header.height), amount: BigInt(supply.amount), - blockId: block.block.id, }); }; @@ -55,10 +58,49 @@ export async function queryTotalSupply(): Promise { export async function _handleSupply(block: CosmosBlock): Promise { const totalSupply = await queryTotalSupply(); if (totalSupply.length === 0) { - logger.warn(`[handleSupply]: no total supply found`); + logger.warn(`[_handleSupply]: no total supply found`); return; } - logger.info(`[handleSupply]: indexing total supply for ${totalSupply.length} coins`); - await store.bulkCreate("Supply", totalSupply.map(supply => getSupplyRecord(supply, block))); + const supplies = await Supply.getByFields( + [["denom", "=", "upokt"]], + { + orderBy: "lastBlockHeight", + orderDirection: "DESC", + limit: 1, + }, + ); + let latestSupply = supplies[0]; + + for (const supply of totalSupply) { + if (supply.denom !== "upokt") { + continue; + } + + if (latestSupply.amount.toString() !== supply.amount) { + latestSupply = getSupplyRecord(supply, block); + await latestSupply.save(); + break; + } + } +} + +export async function _handleBlockSupply(block: CosmosBlock): Promise { + const supplies = await Supply.getByFields( + [["denom", "=", "upokt"]], + { + orderBy: "lastBlockHeight", + orderDirection: "DESC", + limit: 1, + }, + ); + + const dbBlock = await Block.get(block.block.id); + if (!dbBlock) { + throw Error(`[_handleBlockSupply]: block ${block.block.id} not found`); + } + + dbBlock.supplyId = supplies[0].id; + + await dbBlock.save(); } diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index d34f2ab..30558dd 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -23,7 +23,11 @@ import { Transaction, TxStatus, } from "../types"; -import { _handleSupply } from "./bank/supply"; +import { + _handleBlockSupply, + _handleSupply, + getSupplyRecord, +} from "./bank/supply"; import { PREFIX } from "./constants"; import type { Genesis } from "./types/genesis"; import { @@ -48,6 +52,11 @@ export async function handleGenesis(block: CosmosBlock): Promise { logger.info(`[handleGenesis] (block.header.height): indexing genesis block ${block.block.header.height}`); + const supply = genesis.app_state.bank.supply.find(supply => supply.denom === "upokt"); + if (!supply) { + throw new Error("[handleGenesis] Genesis file is missing supply for upokt"); + } + await Promise.all( [ store.bulkCreate("Account", genesis.app_state.auth.accounts.map(account => { @@ -56,6 +65,7 @@ export async function handleGenesis(block: CosmosBlock): Promise { chainId: block.block.header.chainId, }; })), + getSupplyRecord(supply, block).save(), Event.create({ id: "genesis", type: "genesis", @@ -152,6 +162,10 @@ export async function handleEvent(event: CosmosEvent): Promise { await attemptHandling(event, _handleEvent, unprocessedEventHandler); } +export async function handleBlockSupply(block: CosmosBlock): Promise { + await attemptHandling(block, _handleBlockSupply, _handleBlockSupplyError); +} + async function _handleBlock(block: CosmosBlock): Promise { logger.info(`[handleBlock] (block.header.height): indexing block ${block.block.header.height}`); @@ -162,6 +176,7 @@ async function _handleBlock(block: CosmosBlock): Promise { chainId, height: BigInt(height), timestamp, + supplyId: "", // this will later be updated with the proper id of the latest Supply }); await blockEntity.save(); @@ -280,7 +295,6 @@ async function _handleEvent(event: CosmosEvent): Promise { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async function _handleBlockError(err: Error, _: CosmosBlock): Promise { // NB: we won't have persisted any related entities yet. @@ -294,3 +308,8 @@ async function _handleTransactionError(err: Error, tx: CosmosTransaction): Promi async function _handleMessageError(err: Error, msg: CosmosMessage): Promise { await trackUnprocessed(err, primitivesFromMsg(msg)); } + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _handleBlockSupplyError(err: Error, block: CosmosBlock): void { + logger.error(`[handleBlockSupplyError] (block ${block.block.header.height}): ${err.message}`); +} diff --git a/src/mappings/types/genesis.ts b/src/mappings/types/genesis.ts index cc48302..a299144 100644 --- a/src/mappings/types/genesis.ts +++ b/src/mappings/types/genesis.ts @@ -1,5 +1,6 @@ -import { BaseAccountSDKType } from "../../types/proto-interfaces/cosmos/auth/v1beta1/auth"; -import { Balance } from "../../types/proto-interfaces/cosmos/bank/v1beta1/genesis"; +import type { Coin } from "../../client/cosmos/base/v1beta1/coin"; +import type { BaseAccountSDKType } from "../../types/proto-interfaces/cosmos/auth/v1beta1/auth"; +import type { Balance } from "../../types/proto-interfaces/cosmos/bank/v1beta1/genesis"; export interface Genesis { initial_height: number, @@ -9,6 +10,7 @@ export interface Genesis { } bank: { balances: Array + supply: Array }, } } From 7ed0355a156b96f1abebf6ad61b948a42109a976 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Wed, 13 Nov 2024 18:09:51 -0400 Subject: [PATCH 7/8] Use Supply.delete function to mark the end range of that supply once the amount changes. --- src/mappings/bank/supply.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts index b3e309a..bfa202e 100644 --- a/src/mappings/bank/supply.ts +++ b/src/mappings/bank/supply.ts @@ -78,6 +78,7 @@ export async function _handleSupply(block: CosmosBlock): Promise { } if (latestSupply.amount.toString() !== supply.amount) { + await Supply.remove(latestSupply.id); latestSupply = getSupplyRecord(supply, block); await latestSupply.save(); break; From 339b6359f2ff96c3b0b8486752dbabea72bfa7e0 Mon Sep 17 00:00:00 2001 From: "Jorge S. Cuesta" Date: Fri, 15 Nov 2024 20:25:34 -0400 Subject: [PATCH 8/8] Implement an enhanced supply tracking mechanism - Replaced the old block supply handler with a more robust tracking system. - Introduced `SupplyDenom` and `BlockSupply` entities for better denomination supply tracking. - Added logic to handle supply records creation for each block, adjusting for the genesis block. - Adjusted schema to accommodate new entities and relationships. - Removed unused functions and refactored supply-related code for clarity and efficiency. --- project.ts | 6 ---- schema.graphql | 16 ++++++++-- src/mappings/bank/supply.ts | 61 +++++++++++++------------------------ src/mappings/primitives.ts | 54 +++++++++++++++++++++++++------- 4 files changed, 77 insertions(+), 60 deletions(-) diff --git a/project.ts b/project.ts index 1b5be50..91cf376 100644 --- a/project.ts +++ b/project.ts @@ -425,12 +425,6 @@ const project: CosmosProject = { type: "coin_received", } }, - // Supply - { - // this will relate the most recent supply with the most recent block on the database - handler: "handleBlockSupply", - kind: CosmosHandlerKind.Block, - }, ], }, }, diff --git a/schema.graphql b/schema.graphql index ce13ad7..5da93c0 100644 --- a/schema.graphql +++ b/schema.graphql @@ -14,6 +14,11 @@ type EventAttribute @entity { event: Event! } +type SupplyDenom @entity { + # denomination indeed + id: ID! +} + type Block @entity { id: ID! # The block header hash chainId: String! @index @@ -23,7 +28,7 @@ type Block @entity { messages: [Message] @derivedFrom(field: "block") events: [Event] @derivedFrom(field: "block") balancesOfAccountByDenom: [Balance] @derivedFrom(field: "lastUpdatedBlock") - supply: Supply! + supplies: [BlockSupply]! @derivedFrom(field: "block") } type Transaction @entity { @@ -126,6 +131,11 @@ type Supply @entity { id: ID! denom: String! @index amount: BigInt! - lastBlockHeight: BigInt! @index - block: [Block]! @derivedFrom(field: "supply") + blocks: [BlockSupply]! @derivedFrom(field: "supply") +} + +type BlockSupply @entity { + id: ID! + block: Block! + supply: Supply! } diff --git a/src/mappings/bank/supply.ts b/src/mappings/bank/supply.ts index bfa202e..0e3cdc9 100644 --- a/src/mappings/bank/supply.ts +++ b/src/mappings/bank/supply.ts @@ -4,20 +4,19 @@ import type { } from "@subql/types-cosmos"; import type { QueryTotalSupplyResponse } from "cosmjs-types/cosmos/bank/v1beta1/query"; import { - Block, + BlockSupply, Supply, } from "../../types"; import { stringify } from "../utils"; -export const getSupplyId = function(supply: Coin, block: CosmosBlock): string { - return `${supply.denom}@${block.block.id}`; +export const getSupplyId = function(denom: string, height: number): string { + return `${denom}@${height}`; }; export const getSupplyRecord = function(supply: Coin, block: CosmosBlock): Supply { return Supply.create({ - id: getSupplyId(supply, block), + id: getSupplyId(supply.denom, block.header.height), denom: supply.denom, - lastBlockHeight: BigInt(block.block.header.height), amount: BigInt(supply.amount), }); }; @@ -62,46 +61,28 @@ export async function _handleSupply(block: CosmosBlock): Promise { return; } - const supplies = await Supply.getByFields( - [["denom", "=", "upokt"]], - { - orderBy: "lastBlockHeight", - orderDirection: "DESC", - limit: 1, - }, - ); - let latestSupply = supplies[0]; - for (const supply of totalSupply) { - if (supply.denom !== "upokt") { + // get the current blockSupply create on block handler to been able to access the assigned previous supply id + // that will allow us to compare the amount and create a new on if needed. + const blockSupplyId = getSupplyId(supply.denom, block.header.height); + const latestBlockSupply = await BlockSupply.get(blockSupplyId); + if (!latestBlockSupply) { + logger.warn(`[_handleSupply]: no BlockSupply found id=${blockSupplyId}`); continue; } - if (latestSupply.amount.toString() !== supply.amount) { - await Supply.remove(latestSupply.id); - latestSupply = getSupplyRecord(supply, block); - await latestSupply.save(); - break; + const latestDenomSupply = await Supply.get(latestBlockSupply.supplyId); + if (!latestDenomSupply) { + logger.warn(`[_handleSupply]: no total supply found id=${latestBlockSupply.supplyId}`); + continue; } - } -} -export async function _handleBlockSupply(block: CosmosBlock): Promise { - const supplies = await Supply.getByFields( - [["denom", "=", "upokt"]], - { - orderBy: "lastBlockHeight", - orderDirection: "DESC", - limit: 1, - }, - ); - - const dbBlock = await Block.get(block.block.id); - if (!dbBlock) { - throw Error(`[_handleBlockSupply]: block ${block.block.id} not found`); + if (latestDenomSupply.amount.toString() !== supply.amount) { + const newSupply = getSupplyRecord(supply, block); + await newSupply.save(); + latestBlockSupply.supplyId = newSupply.id; + await latestBlockSupply.save(); + break; + } } - - dbBlock.supplyId = supplies[0].id; - - await dbBlock.save(); } diff --git a/src/mappings/primitives.ts b/src/mappings/primitives.ts index 30558dd..ebf57a5 100644 --- a/src/mappings/primitives.ts +++ b/src/mappings/primitives.ts @@ -14,18 +14,20 @@ import { import { Balance, Block, + BlockSupply, Event, EventAttribute, GenesisBalance, GenesisFile as GenesisEntity, Message, NativeBalanceChange, + SupplyDenom, Transaction, TxStatus, } from "../types"; import { - _handleBlockSupply, _handleSupply, + getSupplyId, getSupplyRecord, } from "./bank/supply"; import { PREFIX } from "./constants"; @@ -65,6 +67,11 @@ export async function handleGenesis(block: CosmosBlock): Promise { chainId: block.block.header.chainId, }; })), + store.bulkCreate("SupplyDenom", genesis.app_state.bank.supply.map(supply => { + return { + id: supply.denom, + }; + })), getSupplyRecord(supply, block).save(), Event.create({ id: "genesis", @@ -162,21 +169,51 @@ export async function handleEvent(event: CosmosEvent): Promise { await attemptHandling(event, _handleEvent, unprocessedEventHandler); } -export async function handleBlockSupply(block: CosmosBlock): Promise { - await attemptHandling(block, _handleBlockSupply, _handleBlockSupplyError); -} - async function _handleBlock(block: CosmosBlock): Promise { logger.info(`[handleBlock] (block.header.height): indexing block ${block.block.header.height}`); const { header: { chainId, height, time }, id } = block.block; const timestamp = new Date(time.getTime()); + + const supplyDenom = await SupplyDenom.getByFields([]); + const supplyIdHeight = block.header.height === 1 ? block.header.height : block.header.height - 1; + + const blockSupplies: BlockSupply[] = []; + + if (block.header.height > 1) { + // on any block after genesis, we need to look up for the previous BlockSupply to copy the supply id of the + // right one, then the claim/proof settlement or ibc txs will update to the right supply id if a new one + // is created for this denom@block + for (const supplyDenomItem of supplyDenom) { + const blockSupply = await BlockSupply.get(getSupplyId(supplyDenomItem.id, supplyIdHeight)); + if (!blockSupply) { + logger.warn(`[handleBlock] (block.header.height): missing block supply for ${supplyDenomItem.id} at height ${supplyIdHeight}`); + continue; + } + blockSupplies.push(BlockSupply.create({ + id: getSupplyId(supplyDenomItem.id, block.header.height), + blockId: block.block.id, + supplyId: blockSupply.supplyId, + })); + } + // create all the entries + await store.bulkCreate("BlockSupply", blockSupplies); + } else { + // create a base record for each supply denomination because is the first block. + await store.bulkCreate("BlockSupply", supplyDenom.map((supplyDenomItem) => { + return { + id: getSupplyId(supplyDenomItem.id, block.block.header.height), + blockId: block.block.id, + supplyId: getSupplyId(supplyDenomItem.id, supplyIdHeight), + }; + })); + } + const blockEntity = Block.create({ id, chainId, height: BigInt(height), timestamp, - supplyId: "", // this will later be updated with the proper id of the latest Supply }); await blockEntity.save(); @@ -308,8 +345,3 @@ async function _handleTransactionError(err: Error, tx: CosmosTransaction): Promi async function _handleMessageError(err: Error, msg: CosmosMessage): Promise { await trackUnprocessed(err, primitivesFromMsg(msg)); } - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function _handleBlockSupplyError(err: Error, block: CosmosBlock): void { - logger.error(`[handleBlockSupplyError] (block ${block.block.header.height}): ${err.message}`); -}