diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a1d8144..bb5c4be 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: "16" + node-version-file: '.nvmrc' - run: yarn install - name: Check versions run: | diff --git a/combined.typegen.json b/combined.typegen.json index 9b47ba6..2cd24d2 100644 --- a/combined.typegen.json +++ b/combined.typegen.json @@ -4,6 +4,8 @@ "typesBundle": "indexer/typesBundle.json", "events": [ "BTCRelay.StoreMainChainHeader", + "DexGeneral.AssetSwap", + "DexStable.CurrencyExchange", "Escrow.Deposit", "Escrow.Withdraw", "Issue.CancelIssue", @@ -30,16 +32,14 @@ "Tokens.Transfer", "VaultRegistry.DecreaseLockedCollateral", "VaultRegistry.IncreaseLockedCollateral", - "VaultRegistry.RegisterVault", - "DexGeneral.AssetSwap", - "DexGeneral.LiquidityAdded", - "DexGeneral.LiquidityRemoved" + "VaultRegistry.RegisterVault" ], "calls": [ "BTCRelay.store_block_header", "System.set_storage" ], "storage": [ + "DexStable.Pools", "Issue.IssuePeriod", "Redeem.RedeemPeriod" ] diff --git a/db/migrations/1675434206425-Data.js b/db/migrations/1675434206425-Data.js new file mode 100644 index 0000000..00bd78d --- /dev/null +++ b/db/migrations/1675434206425-Data.js @@ -0,0 +1,15 @@ +module.exports = class Data1675434206425 { + name = 'Data1675434206425' + + async up(db) { + await db.query(`CREATE TABLE "cumulative_dex_trading_volume_per_pool" ("id" character varying NOT NULL, "pool_id" text NOT NULL, "pool_type" character varying(8) NOT NULL, "till_timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "amounts" jsonb NOT NULL, CONSTRAINT "PK_c9bb1ee57bff1390d948e3e6f12" PRIMARY KEY ("id"))`) + await db.query(`CREATE INDEX "IDX_bd6bc9a6ce9e1fcb81b0650c0f" ON "cumulative_dex_trading_volume_per_pool" ("pool_id") `) + await db.query(`CREATE INDEX "IDX_a903319c2555960f188406a839" ON "cumulative_dex_trading_volume_per_pool" ("till_timestamp") `) + } + + async down(db) { + await db.query(`DROP TABLE "cumulative_dex_trading_volume_per_pool"`) + await db.query(`DROP INDEX "public"."IDX_bd6bc9a6ce9e1fcb81b0650c0f"`) + await db.query(`DROP INDEX "public"."IDX_a903319c2555960f188406a839"`) + } +} diff --git a/distributable/schema.graphql b/distributable/schema.graphql index 06d2b02..5abe633 100644 --- a/distributable/schema.graphql +++ b/distributable/schema.graphql @@ -243,6 +243,26 @@ type CumulativeVolumePerCurrencyPair @entity { collateralCurrency: Currency } +type PooledAmount { + amount: BigInt! + amountHuman: BigDecimal + token: PooledToken! + # TODO: check if this should be Currency? +} + +enum PoolType { + Standard + Stable +} + +type CumulativeDexTradingVolumePerPool @entity { + id: ID! + poolId: String! @index + poolType: PoolType! + tillTimestamp: DateTime! @index + amounts: [PooledAmount!]! +} + type IssuePeriod @entity { height: Height! timestamp: DateTime! diff --git a/package.json b/package.json index d4d5c1d..19baa49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "interbtc-indexer", "private": "true", - "version": "0.14.1", + "version": "0.14.2", "description": "GraphQL server and Substrate indexer for the interBTC parachain", "author": "", "license": "ISC", diff --git a/schema.graphql b/schema.graphql index 06d2b02..5abe633 100644 --- a/schema.graphql +++ b/schema.graphql @@ -243,6 +243,26 @@ type CumulativeVolumePerCurrencyPair @entity { collateralCurrency: Currency } +type PooledAmount { + amount: BigInt! + amountHuman: BigDecimal + token: PooledToken! + # TODO: check if this should be Currency? +} + +enum PoolType { + Standard + Stable +} + +type CumulativeDexTradingVolumePerPool @entity { + id: ID! + poolId: String! @index + poolType: PoolType! + tillTimestamp: DateTime! @index + amounts: [PooledAmount!]! +} + type IssuePeriod @entity { height: Height! timestamp: DateTime! diff --git a/src/mappings/_utils.ts b/src/mappings/_utils.ts index 7f54335..6fd9de8 100644 --- a/src/mappings/_utils.ts +++ b/src/mappings/_utils.ts @@ -80,7 +80,7 @@ export async function getForeignAsset(id: number): Promise { } try { const wsProvider = new WsProvider(process.env.CHAIN_ENDPOINT); - const api = await ApiPromise.create({ provider: wsProvider }); + const api = await ApiPromise.create({ provider: wsProvider, noInitWarn: true }); const assets = await api.query.assetRegistry.metadata(id); const assetsJSON = assets.toHuman(); const metadata = assetsJSON as AssetMetadata; @@ -170,4 +170,9 @@ export async function convertAmountToHuman(currency: Currency, amount: bigint ) const currencyInfo: CurrencyExt = await currencyToLibCurrencyExt(currency); const monetaryAmount = newMonetaryAmount(amount.toString(), currencyInfo); return BigDecimal(monetaryAmount.toString()); +} + +// helper method to switch around key/value pairs for a given map +export function invertMap(map: Map): Map { + return new Map(Array.from(map, ([key, value]) => [value, key])); } \ No newline at end of file diff --git a/src/mappings/event/dex.ts b/src/mappings/event/dex.ts new file mode 100644 index 0000000..af7ef10 --- /dev/null +++ b/src/mappings/event/dex.ts @@ -0,0 +1,177 @@ +import { SubstrateBlock } from "@subsquid/substrate-processor"; +import { CumulativeDexTradingVolumePerPool, Currency, fromJsonPooledToken, PooledToken } from "../../model"; +import { Ctx, EventItem } from "../../processor"; +import { DexGeneralAssetSwapEvent, DexStableCurrencyExchangeEvent } from "../../types/events"; +import { currencyId } from "../encoding"; +import { SwapDetails, updateCumulativeDexVolumesForStablePool, updateCumulativeDexVolumesForStandardPool } from "../utils/cumulativeVolumes"; +import EntityBuffer from "../utils/entityBuffer"; +import { getStablePoolCurrencyByIndex } from "../utils/pools"; + +function isPooledToken(currency: Currency): currency is PooledToken { + try { + fromJsonPooledToken(currency); + return true; + } catch (e) { + return false; + } +} + +/** + * Combines the given arrays into in/out pairs as an array of {@link SwapDetails}. + * @param currencies The currencies in the swap path + * @param atomicBalances The swapped balances, in atomic units and same order as currencies + * @returns An array of pair-wise combined {@link SwapDetails}. + * @throws {@link Error} + * Throws an error if currencies length does not match balances length, or if a passed in currency is not a {@link PooledToken} + */ +function createPairWiseSwapDetails(currencies: Currency[], atomicBalances: bigint[]): SwapDetails[] { + if (currencies.length !== atomicBalances.length) { + throw new Error(`Cannot combine pair wise swap details; currency count [${ + currencies.length + }] does not match balance count [${ + atomicBalances.length + }]`); + } + + const swapDetailsList: SwapDetails[] = []; + for(let idx = 0; (idx + 1) < currencies.length; idx++) { + const inIdx = idx; + const outIdx = idx + 1; + const currencyIn = currencies[inIdx]; + const currencyOut = currencies[outIdx]; + + if (!isPooledToken(currencyIn)) { + throw new Error(`Cannot combine pair wise swap details; unexpected currency type ${ + currencyIn.isTypeOf + } in pool, skip processing of DexGeneralAssetSwapEvent`); + } else if (!isPooledToken(currencyOut)) { + throw new Error(`Unexpected currency type ${ + currencyOut.isTypeOf + } in pool, skip processing of DexGeneralAssetSwapEvent`); + } + + swapDetailsList.push({ + from: { + currency: currencyIn, + atomicAmount: atomicBalances[inIdx] + }, + to: { + currency: currencyOut, + atomicAmount: atomicBalances[outIdx] + } + }); + } + + return swapDetailsList; +} + +export async function dexGeneralAssetSwap( + ctx: Ctx, + block: SubstrateBlock, + item: EventItem, + entityBuffer: EntityBuffer +): Promise { + const rawEvent = new DexGeneralAssetSwapEvent(ctx, item.event); + let currencies: Currency[] = []; + let atomicBalances: bigint[] = []; + + if (rawEvent.isV1021000) { + const [, , swapPath, balances] = rawEvent.asV1021000; + currencies = swapPath.map(currencyId.encode); + atomicBalances = balances; + } else { + ctx.log.warn("UNKOWN EVENT VERSION: DexGeneral.AssetSwap"); + return; + } + + // we can only use pooled tokens, check we have not other ones + for (const currency of currencies) { + if (!isPooledToken(currency)) { + ctx.log.error(`Unexpected currency type ${currency.isTypeOf} in pool, skip processing of DexGeneralAssetSwapEvent`); + return; + } + } + + let swapDetailsList: SwapDetails[]; + try { + swapDetailsList = createPairWiseSwapDetails(currencies, atomicBalances); + } catch (e) { + ctx.log.error((e as Error).message); + return; + } + + // construct and await sequentially, otherwise some operations may try to read values from + // the entity buffer before it has been updated + for (const swapDetails of swapDetailsList) { + const entity = await updateCumulativeDexVolumesForStandardPool( + ctx.store, + new Date(block.timestamp), + swapDetails, + entityBuffer + ); + + entityBuffer.pushEntity(CumulativeDexTradingVolumePerPool.name, entity); + } +} + +export async function dexStableCurrencyExchange( + ctx: Ctx, + block: SubstrateBlock, + item: EventItem, + entityBuffer: EntityBuffer +): Promise { + const rawEvent = new DexStableCurrencyExchangeEvent(ctx, item.event); + let poolId: number; + let inIndex: number; + let outIndex: number; + let inAmount: bigint; + let outAmount: bigint; + + if (rawEvent.isV1021000) { + const event = rawEvent.asV1021000; + poolId = event.poolId; + inIndex = event.inIndex; + outIndex = event.outIndex; + inAmount = event.inAmount; + outAmount = event.outAmount; + } else { + ctx.log.warn("UNKOWN EVENT VERSION: DexStable.CurrencyExchange"); + return; + } + + const outCurrency = await getStablePoolCurrencyByIndex(ctx, block, poolId, outIndex); + const inCurrency = await getStablePoolCurrencyByIndex(ctx, block, poolId, inIndex); + + if (!isPooledToken(inCurrency)) { + ctx.log.error(`Unexpected currencyIn type ${inCurrency.isTypeOf}, skip processing of DexGeneralAssetSwapEvent`); + return; + } + if (!isPooledToken(outCurrency)) { + ctx.log.error(`Unexpected currencyOut type ${outCurrency.isTypeOf}, skip processing of DexGeneralAssetSwapEvent`); + return; + } + + const swapDetails: SwapDetails = { + from: { + currency: inCurrency, + atomicAmount: inAmount + }, + to: { + currency: outCurrency, + atomicAmount: outAmount + } + }; + + const entityPromise = updateCumulativeDexVolumesForStablePool( + ctx.store, + new Date(block.timestamp), + poolId, + swapDetails, + entityBuffer + ); + + entityBuffer.pushEntity( + CumulativeDexTradingVolumePerPool.name, + await entityPromise + ); +} \ No newline at end of file diff --git a/src/mappings/event/index.ts b/src/mappings/event/index.ts index b1da592..a943f5b 100644 --- a/src/mappings/event/index.ts +++ b/src/mappings/event/index.ts @@ -1,3 +1,4 @@ +export * from "./dex"; export * from "./issue"; export * from "./redeem"; export * from "./vault"; diff --git a/src/mappings/event/loans.ts b/src/mappings/event/loans.ts index 7112eb6..2cd2747 100644 --- a/src/mappings/event/loans.ts +++ b/src/mappings/event/loans.ts @@ -176,8 +176,6 @@ export async function activatedMarket( marketDb.activation = activation; await entityBuffer.pushEntity(LoanMarketActivation.name, activation); await entityBuffer.pushEntity(LoanMarket.name, marketDb); - - console.log(`Activated ${marketDb.id}`); } export async function borrow( diff --git a/src/mappings/utils/cumulativeVolumes.ts b/src/mappings/utils/cumulativeVolumes.ts index f336bd1..b45d6bf 100644 --- a/src/mappings/utils/cumulativeVolumes.ts +++ b/src/mappings/utils/cumulativeVolumes.ts @@ -1,14 +1,18 @@ import { + CumulativeDexTradingVolumePerPool, CumulativeVolume, CumulativeVolumePerCurrencyPair, Currency, + PooledAmount, + PooledToken, + PoolType, VolumeType, } from "../../model"; import { Equal, LessThanOrEqual } from "typeorm"; import { Store } from "@subsquid/typeorm-store"; -import { EventItem } from "../../processor"; import EntityBuffer from "./entityBuffer"; import { convertAmountToHuman } from "../_utils"; +import { inferGeneralPoolId } from "./pools"; function getLatestCurrencyPairCumulativeVolume( cumulativeVolumes: CumulativeVolumePerCurrencyPair[], @@ -201,3 +205,131 @@ export async function updateCumulativeVolumesForCurrencyPair( return cumulativeVolumeForCollateral; } } + +type SwapDetailsAmount = { + currency: PooledToken, + atomicAmount: bigint +}; + +export type SwapDetails = { + from: SwapDetailsAmount, + to: SwapDetailsAmount +} + +async function createPooledAmount(swapAmount: SwapDetailsAmount): Promise { + const amountHuman = await convertAmountToHuman(swapAmount.currency, swapAmount.atomicAmount); + return new PooledAmount({ + token: swapAmount.currency, + amount: swapAmount.atomicAmount, + amountHuman + }); +} + +/** + * Modifies the passed in entity to add or update pooled token volumes. + * @param entity The entity to add pooled amounts to, or update existing ones + * @param swapDetails The swap details used to modify the event + * @return Returns the modified entity + */ +async function updateOrAddPooledAmounts(entity: CumulativeDexTradingVolumePerPool, swapDetails: SwapDetails): Promise { + // we need to find & update existing amounts, or add new ones + let foundFrom = false; + let foundTo = false; + + for (const pooledAmount of entity.amounts) { + if (foundFrom && foundTo) { + // done: exit loop + break; + } + + let amountToAdd: bigint | undefined = undefined; + if (pooledAmount.token === swapDetails.from.currency) { + amountToAdd = swapDetails.from.atomicAmount; + foundFrom = true; + } else if (pooledAmount.token === swapDetails.to.currency) { + amountToAdd = swapDetails.to.atomicAmount; + foundTo = true; + } + + // update volume in place (ie. modify the entity directly) + if (amountToAdd) { + const newAmount = pooledAmount.amount + swapDetails.to.atomicAmount; + pooledAmount.amount = newAmount; + pooledAmount.amountHuman = await convertAmountToHuman(pooledAmount.token, newAmount); + } + } + + // if we get here, there is at least one new pooled amount to add to the entity + if (!foundFrom) { + const pooledAmount = await createPooledAmount(swapDetails.from); + entity.amounts.push(pooledAmount); + } + if (!foundTo) { + const pooledAmount = await createPooledAmount(swapDetails.to); + entity.amounts.push(pooledAmount); + } + + return entity; +} + +async function fetchOrCreateEntity( + entityId: string, + poolId: string, + poolType: PoolType, + tillTimestamp: Date, + store: Store, + entityBuffer: EntityBuffer +): Promise { + // fetch from buffer if it exists + return (entityBuffer.getBufferedEntityBy( + CumulativeDexTradingVolumePerPool.name, + entityId + ) as CumulativeDexTradingVolumePerPool | undefined) || + // if not found, try to fetch from store + (await store.get(CumulativeDexTradingVolumePerPool, entityId)) || + // still not found, create a new entity + new CumulativeDexTradingVolumePerPool({ + id: entityId, + poolId: poolId, + poolType, + tillTimestamp, + amounts: [] + }); +} + +function buildPoolEntityId(poolId: string, poolType: PoolType, timestamp: Date): string { + return `${poolId}-${poolType}-${timestamp + .getTime() + .toString()}`; +} + +export async function updateCumulativeDexVolumesForStandardPool( + store: Store, + timestamp: Date, + swapDetails: SwapDetails, + entityBuffer: EntityBuffer +): Promise { + const poolType = PoolType.Standard; + + const poolId = inferGeneralPoolId(swapDetails.from.currency, swapDetails.to.currency); + + const entityId = buildPoolEntityId(poolId, poolType, timestamp); + + const entity = await fetchOrCreateEntity(entityId, poolId, poolType, timestamp, store, entityBuffer); + return await updateOrAddPooledAmounts(entity, swapDetails); +} + +export async function updateCumulativeDexVolumesForStablePool( + store: Store, + timestamp: Date, + poolId: number, + swapDetails: SwapDetails, + entityBuffer: EntityBuffer +): Promise { + const poolType = PoolType.Stable; + + const entityId = buildPoolEntityId(poolId.toString(), poolType, timestamp); + + const entity = await fetchOrCreateEntity(entityId, poolId.toString(), poolType, timestamp, store, entityBuffer); + return await updateOrAddPooledAmounts(entity, swapDetails); +} \ No newline at end of file diff --git a/src/mappings/utils/pools.ts b/src/mappings/utils/pools.ts new file mode 100644 index 0000000..5acbfcd --- /dev/null +++ b/src/mappings/utils/pools.ts @@ -0,0 +1,168 @@ +import { SubstrateBlock } from "@subsquid/substrate-processor"; +import { Currency, Token } from "../../model"; +import { Ctx } from "../../processor"; +import { DexStablePoolsStorage } from "../../types/storage"; +import { CurrencyId, Pool, Pool_Base, Pool_Meta } from "../../types/v1021000"; +import { currencyToString, currencyId as currencyEncoder } from "../encoding"; +import { invertMap } from "../_utils"; + +const indexToCurrencyTypeMap: Map = new Map([ + [0, "NativeToken"], + [1, "ForeignAsset"], + [2, "LendToken"], + [3, "LpToken"], + [4, "StableLpToken"] +]); +const currencyTypeToIndexMap = invertMap(indexToCurrencyTypeMap); + +// Replicated order from parachain code. +// See also https://github.com/interlay/interbtc/blob/d48fee47e153291edb92525221545c2f4fa58501/primitives/src/lib.rs#L469-L476 +const indexToNativeTokenMap: Map = new Map([ + [0, Token.DOT], + [1, Token.IBTC], + [2, Token.INTR], + [10, Token.KSM], + [11, Token.KBTC], + [12, Token.KINT] +]); + +const nativeTokenToIndexMap = invertMap(indexToNativeTokenMap); + +// poor man's stable pool id to currencies cache +const stablePoolCurrenciesCache = new Map(); + +function setPoolCurrencies(poolId: number, currencies: Currency[]) { + stablePoolCurrenciesCache.set(poolId, currencies); +} + +export function clearPoolCurrencies() { + stablePoolCurrenciesCache.clear(); +} + +export function isBasePool(pool: Pool): pool is Pool_Base { + return pool.__kind === "Base"; +} + +export function isMetaPool(pool: Pool): pool is Pool_Meta { + return pool.__kind === "Meta"; +} + +export async function getStablePoolCurrencyByIndex(ctx: Ctx, block: SubstrateBlock, poolId: number, index: number): Promise { + if (stablePoolCurrenciesCache.has(poolId)) { + const currencies = stablePoolCurrenciesCache.get(poolId)!; + if (currencies.length > index) { + return currencies[index]; + } + } + + // (attempt to) fetch from storage + const rawPoolStorage = new DexStablePoolsStorage(ctx, block); + if (!rawPoolStorage.isExists) { + throw Error("getStablePoolCurrencyByIndex: DexStable.Pools storage is not defined for this spec version"); + } else if (rawPoolStorage.isV1021000) { + const pool = await rawPoolStorage.getAsV1021000(poolId); + let currencies: Currency[] = []; + // check pool is found and as a BasePool + if (pool == undefined ) { + throw Error(`getStablePoolCurrencyByIndex: Unable to find stable pool in storage for given poolId [${poolId}]`); + } else if (isBasePool(pool)) { + const basePoolCurrencyIds = pool.value.currencyIds; + currencies = basePoolCurrencyIds.map(currencyId => currencyEncoder.encode(currencyId)); + } else if (isMetaPool(pool)) { + const metaPoolCurrencyIds = pool.value.info.currencyIds; + currencies = metaPoolCurrencyIds.map(currencyId => currencyEncoder.encode(currencyId)); + } else { + // use of any to future-proof for if/when pool types are expanded. + throw Error(`getStablePoolCurrencyByIndex: Found pool for given poolId [${poolId}], but it is an unexpected pool type [${(pool as any).__kind}]`); + } + + setPoolCurrencies(poolId, currencies); + + if (currencies.length > index) { + return currencies[index]; + } + } else { + throw Error("getStablePoolCurrencyByIndex: Unknown DexStablePoolsStorage version"); + } + + throw Error(`getStablePoolCurrencyByIndex: Unable to find currency in DexStablePoolsStorage for given poolId [${poolId}] and currency index [${index}]`); +} + +function compareCurrencyType(currency0: Currency, currency1: Currency): number { + if (currency0.isTypeOf === currency1.isTypeOf) { + return 0; + } + + const typeIndex0 = currencyTypeToIndexMap.get(currency0.isTypeOf); + const typeIndex1 = currencyTypeToIndexMap.get(currency1.isTypeOf); + + if (typeIndex0 === undefined) { + throw Error(`Unable to find index for given currency type [${currency0.isTypeOf}]`); + } + if (typeIndex1 === undefined) { + throw Error(`Unable to find index for given currency type [${currency1.isTypeOf}]`); + } + + return typeIndex0 - typeIndex1; +} + +function currencyToIndex(currency: Currency): number { + switch(currency.isTypeOf) { + case "NativeToken": + const tokenIndex = nativeTokenToIndexMap.get(currency.token); + if (tokenIndex === undefined) { + throw Error(`currencyToIndex: Unknown or unhandled native token [${currency.token.toString()}]`); + } + return tokenIndex; + case "ForeignAsset": + return currency.asset; + case "LendToken": + return currency.lendTokenId; + case "StableLpToken": + return currency.poolId; + default: + throw Error(`currencyToIndex: Unknown or unsupported currency type [${currency.isTypeOf}]`); + } +} + +/** + * For sorting currencies. + * @param currency0 first currency + * @param currency1 second currency + * @returns A negative number if currency0 should be listed before currency1, + * a positive number if currency1 should be listed before currency0, + * otherwise returns 0 + */ +function compareCurrencies(currency0: Currency, currency1: Currency): number { + const typeCompare = compareCurrencyType(currency0, currency1); + if (typeCompare != 0) { + return typeCompare; + } + + const index0 = currencyToIndex(currency0); + const index1 = currencyToIndex(currency1); + return index0 - index1; +} + +/** + * Calculate the standard pool's id given 2 currencies. + * This method will sort the currencies and return their ids/tickers in a specific order. + * + * @param currency0 One currency + * @param currency1 The other currency + */ +export function inferGeneralPoolId(currency0: Currency, currency1: Currency): string { + let firstCurrencyString: string = currencyToString(currency0); + let secondCurrencyString: string = currencyToString(currency1); + + const order: number = compareCurrencies(currency0, currency1); + // swap strings around if needed + if (order > 0) { + // works because strings (ie. not the capitalized String) are passed by value + const hold = firstCurrencyString; + firstCurrencyString = secondCurrencyString; + secondCurrencyString = hold; + } + + return `(${firstCurrencyString},${secondCurrencyString})`; +} \ No newline at end of file diff --git a/src/model/generated/_poolType.ts b/src/model/generated/_poolType.ts new file mode 100644 index 0000000..6d762ad --- /dev/null +++ b/src/model/generated/_poolType.ts @@ -0,0 +1,4 @@ +export enum PoolType { + Standard = "Standard", + Stable = "Stable", +} diff --git a/src/model/generated/_pooledAmount.ts b/src/model/generated/_pooledAmount.ts new file mode 100644 index 0000000..2a3cb4a --- /dev/null +++ b/src/model/generated/_pooledAmount.ts @@ -0,0 +1,53 @@ +import {BigDecimal} from "@subsquid/big-decimal" +import assert from "assert" +import * as marshal from "./marshal" +import {PooledToken, fromJsonPooledToken} from "./_pooledToken" + +export class PooledAmount { + private _amount!: bigint + private _amountHuman!: BigDecimal | undefined | null + private _token!: PooledToken + + constructor(props?: Partial>, json?: any) { + Object.assign(this, props) + if (json != null) { + this._amount = marshal.bigint.fromJSON(json.amount) + this._amountHuman = json.amountHuman == null ? undefined : marshal.bigdecimal.fromJSON(json.amountHuman) + this._token = fromJsonPooledToken(json.token) + } + } + + get amount(): bigint { + assert(this._amount != null, 'uninitialized access') + return this._amount + } + + set amount(value: bigint) { + this._amount = value + } + + get amountHuman(): BigDecimal | undefined | null { + return this._amountHuman + } + + set amountHuman(value: BigDecimal | undefined | null) { + this._amountHuman = value + } + + get token(): PooledToken { + assert(this._token != null, 'uninitialized access') + return this._token + } + + set token(value: PooledToken) { + this._token = value + } + + toJSON(): object { + return { + amount: marshal.bigint.toJSON(this.amount), + amountHuman: this.amountHuman == null ? undefined : marshal.bigdecimal.toJSON(this.amountHuman), + token: this.token.toJSON(), + } + } +} diff --git a/src/model/generated/cumulativeDexTradingVolumePerPool.model.ts b/src/model/generated/cumulativeDexTradingVolumePerPool.model.ts new file mode 100644 index 0000000..8e9f465 --- /dev/null +++ b/src/model/generated/cumulativeDexTradingVolumePerPool.model.ts @@ -0,0 +1,28 @@ +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, Index as Index_} from "typeorm" +import * as marshal from "./marshal" +import {PoolType} from "./_poolType" +import {PooledAmount} from "./_pooledAmount" + +@Entity_() +export class CumulativeDexTradingVolumePerPool { + constructor(props?: Partial) { + Object.assign(this, props) + } + + @PrimaryColumn_() + id!: string + + @Index_() + @Column_("text", {nullable: false}) + poolId!: string + + @Column_("varchar", {length: 8, nullable: false}) + poolType!: PoolType + + @Index_() + @Column_("timestamp with time zone", {nullable: false}) + tillTimestamp!: Date + + @Column_("jsonb", {transformer: {to: obj => obj.map((val: any) => val.toJSON()), from: obj => obj == null ? undefined : marshal.fromList(obj, val => new PooledAmount(undefined, marshal.nonNull(val)))}, nullable: false}) + amounts!: (PooledAmount)[] +} diff --git a/src/model/generated/index.ts b/src/model/generated/index.ts index 5d85395..7c1d50c 100644 --- a/src/model/generated/index.ts +++ b/src/model/generated/index.ts @@ -27,6 +27,9 @@ export * from "./_oracleUpdateType" export * from "./cumulativeVolume.model" export * from "./_volumeType" export * from "./cumulativeVolumePerCurrencyPair.model" +export * from "./cumulativeDexTradingVolumePerPool.model" +export * from "./_poolType" +export * from "./_pooledAmount" export * from "./issuePeriod.model" export * from "./redeemPeriod.model" export * from "./transfer.model" diff --git a/src/processor.ts b/src/processor.ts index 75721d3..1524d0d 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -16,6 +16,8 @@ import { cancelIssue, cancelRedeem, decreaseLockedCollateral, + dexGeneralAssetSwap, + dexStableCurrencyExchange, executeIssue, executeRedeem, feedValues, @@ -69,6 +71,8 @@ const processor = new SubstrateBatchProcessor() .setTypesBundle("indexer/typesBundle.json") .setBlockRange({ from: processFrom }) .addEvent("BTCRelay.StoreMainChainHeader", eventArgsData) + .addEvent("DexGeneral.AssetSwap", eventArgsData) + .addEvent("DexStable.CurrencyExchange", eventArgsData) .addEvent("Escrow.Deposit", eventArgsData) .addEvent("Escrow.Withdraw", eventArgsData) .addEvent("Issue.CancelIssue", eventArgsData) @@ -271,6 +275,16 @@ processor.run(new TypeormDatabase({ stateSchema: "interbtc" }), async (ctx) => { mapping: withdraw, totalTime: 0, }, + { + filter: { name: "DexGeneral.AssetSwap" }, + mapping: dexGeneralAssetSwap, + totalTime: 0 + }, + { + filter: { name: "DexStable.CurrencyExchange" }, + mapping: dexStableCurrencyExchange, + totalTime: 0 + } ]); // second stage diff --git a/src/types/events.ts b/src/types/events.ts index 0ec3f82..989bcf9 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -92,7 +92,7 @@ export class DexGeneralAssetSwapEvent { } } -export class DexGeneralLiquidityAddedEvent { +export class DexStableCurrencyExchangeEvent { private readonly _chain: Chain private readonly event: Event @@ -100,55 +100,22 @@ export class DexGeneralLiquidityAddedEvent { constructor(ctx: ChainContext, event: Event) constructor(ctx: EventContext, event?: Event) { event = event || ctx.event - assert(event.name === 'DexGeneral.LiquidityAdded') + assert(event.name === 'DexStable.CurrencyExchange') this._chain = ctx._chain this.event = event } /** - * Add liquidity. \[owner, asset_0, asset_1, add_balance_0, add_balance_1, - * mint_balance_lp\] + * Swap a amounts of currency to get other. */ get isV1021000(): boolean { - return this._chain.getEventHash('DexGeneral.LiquidityAdded') === 'd8b087aac9964db76a860392438c8c03122c1821fc97316158cf5177e3078899' + return this._chain.getEventHash('DexStable.CurrencyExchange') === '0abe856f4fa3b499a28439696d7ae07664a33ec25834861bc9032b5d4950a766' } /** - * Add liquidity. \[owner, asset_0, asset_1, add_balance_0, add_balance_1, - * mint_balance_lp\] + * Swap a amounts of currency to get other. */ - get asV1021000(): [Uint8Array, v1021000.CurrencyId, v1021000.CurrencyId, bigint, bigint, bigint] { - assert(this.isV1021000) - return this._chain.decodeEvent(this.event) - } -} - -export class DexGeneralLiquidityRemovedEvent { - private readonly _chain: Chain - private readonly event: Event - - constructor(ctx: EventContext) - constructor(ctx: ChainContext, event: Event) - constructor(ctx: EventContext, event?: Event) { - event = event || ctx.event - assert(event.name === 'DexGeneral.LiquidityRemoved') - this._chain = ctx._chain - this.event = event - } - - /** - * Remove liquidity. \[owner, recipient, asset_0, asset_1, rm_balance_0, rm_balance_1, - * burn_balance_lp\] - */ - get isV1021000(): boolean { - return this._chain.getEventHash('DexGeneral.LiquidityRemoved') === '3b79687d35ae212367d8e45de1258467b263a0005a6840dceaf3184c4aad8999' - } - - /** - * Remove liquidity. \[owner, recipient, asset_0, asset_1, rm_balance_0, rm_balance_1, - * burn_balance_lp\] - */ - get asV1021000(): [Uint8Array, Uint8Array, v1021000.CurrencyId, v1021000.CurrencyId, bigint, bigint, bigint] { + get asV1021000(): {poolId: number, who: Uint8Array, to: Uint8Array, inIndex: number, inAmount: bigint, outIndex: number, outAmount: bigint} { assert(this.isV1021000) return this._chain.decodeEvent(this.event) } diff --git a/src/types/storage.ts b/src/types/storage.ts index 687ebe7..63a3d8e 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -1,5 +1,51 @@ import assert from 'assert' import {Block, Chain, ChainContext, BlockContext, Result, Option} from './support' +import * as v1021000 from './v1021000' + +export class DexStablePoolsStorage { + private readonly _chain: Chain + private readonly blockHash: string + + constructor(ctx: BlockContext) + constructor(ctx: ChainContext, block: Block) + constructor(ctx: BlockContext, block?: Block) { + block = block || ctx.block + this.blockHash = block.hash + this._chain = ctx._chain + } + + /** + * Info of a pool. + */ + get isV1021000() { + return this._chain.getStorageItemTypeHash('DexStable', 'Pools') === '589bc6643ae522e9f672bbb153dcc85f9ed48dd356b43697ce45ce589424599a' + } + + /** + * Info of a pool. + */ + async getAsV1021000(key: number): Promise { + assert(this.isV1021000) + return this._chain.getStorage(this.blockHash, 'DexStable', 'Pools', key) + } + + async getManyAsV1021000(keys: number[]): Promise<(v1021000.Pool | undefined)[]> { + assert(this.isV1021000) + return this._chain.queryStorage(this.blockHash, 'DexStable', 'Pools', keys.map(k => [k])) + } + + async getAllAsV1021000(): Promise<(v1021000.Pool)[]> { + assert(this.isV1021000) + return this._chain.queryStorage(this.blockHash, 'DexStable', 'Pools') + } + + /** + * Checks whether the storage item is defined for the current chain version. + */ + get isExists(): boolean { + return this._chain.getStorageItemTypeHash('DexStable', 'Pools') != null + } +} export class IssueIssuePeriodStorage { private readonly _chain: Chain diff --git a/src/types/v1021000.ts b/src/types/v1021000.ts index 2efdb89..2463e70 100644 --- a/src/types/v1021000.ts +++ b/src/types/v1021000.ts @@ -103,6 +103,18 @@ export interface VaultCurrencyPair { wrapped: CurrencyId } +export type Pool = Pool_Base | Pool_Meta + +export interface Pool_Base { + __kind: 'Base' + value: BasePool +} + +export interface Pool_Meta { + __kind: 'Meta' + value: MetaPool +} + export type TokenSymbol = TokenSymbol_DOT | TokenSymbol_IBTC | TokenSymbol_INTR | TokenSymbol_KSM | TokenSymbol_KBTC | TokenSymbol_KINT export interface TokenSymbol_DOT { @@ -172,6 +184,31 @@ export interface MarketState_Supervision { __kind: 'Supervision' } +export interface BasePool { + currencyIds: CurrencyId[] + lpCurrencyId: CurrencyId + tokenMultipliers: bigint[] + balances: bigint[] + fee: bigint + adminFee: bigint + initialA: bigint + futureA: bigint + initialATime: bigint + futureATime: bigint + account: Uint8Array + adminFeeReceiver: Uint8Array + lpCurrencySymbol: Uint8Array + lpCurrencyDecimal: number +} + +export interface MetaPool { + basePoolId: number + baseVirtualPrice: bigint + baseCacheLastUpdated: bigint + baseCurrencies: CurrencyId[] + info: BasePool +} + export interface JumpModel { baseRate: bigint jumpRate: bigint