diff --git a/package.json b/package.json index 7a5bd33e..4bf154b5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "dependencies": { "@subql/types-cosmos": "^3.2.3", "@types/node": "^17.0.21", + "bech32": "^2.0.0", + "js-sha256": "^0.11.0", "pino": "^7.8.0", "ts-proto": "^1.112.1", "tslib": "^2.3.1" diff --git a/project.ts b/project.ts index 39aaf9dc..36fa2430 100644 --- a/project.ts +++ b/project.ts @@ -80,6 +80,26 @@ const project: CosmosProject = { // }, // }, // }, + { + handler: "handleIbcSendPacketEvent", + kind: CosmosHandlerKind.Event, + filter: { + type: "send_packet", + messageFilter: { + type: "/ibc.applications.transfer.v1.MsgTransfer", + }, + }, + }, + { + handler: "handleIbcReceivePacketEvent", + kind: CosmosHandlerKind.Event, + filter: { + type: "recv_packet", + messageFilter: { + type: "/ibc.core.channel.v1.MsgRecvPacket", + }, + }, + }, { handler: "handleStateChangeEvent", kind: CosmosHandlerKind.Event, diff --git a/schema.graphql b/schema.graphql index a563d048..62fedbba 100644 --- a/schema.graphql +++ b/schema.graphql @@ -241,3 +241,30 @@ type ReserveMetrics @entity { totalFeeMinted: BigInt! allocations: [ReserveAllocationMetrics] @derivedFrom(field: "reserveMetrics") } + +type Account @entity { + id: ID! +} + +type IBCChannel @entity { + id: ID! + channelName: String! + escrowAccount: Account! +} + +enum TransferType { + SEND + RECEIVE +} + +type IBCTransfer @entity { + id: ID! + blockTime: Date! + blockHeight: BigInt! + channel: IBCChannel! + srcAccount: Account! + destAccount: Account! + denom: String! + amount: String! + transferType: TransferType! +} diff --git a/src/mappings/constants.ts b/src/mappings/constants.ts index 465b9302..d9b27544 100644 --- a/src/mappings/constants.ts +++ b/src/mappings/constants.ts @@ -17,12 +17,16 @@ export const EVENT_TYPES = { STORAGE: "storage", SUBMIT_PROPOSAL: "submit_proposal", TRANSFER: "transfer", + SEND_PACKET: "send_packet", + RECEIVE_PACKET: "recv_packet", + IBC_TRANSFER: "ibc_transfer", + FUNGIBLE_TOKEN_PACKET: "fungible_token_packet", }; export const VAULT_STATES = { LIQUIDATING: "liquidating", - LIQUIDATED: "liquidated" -} + LIQUIDATED: "liquidated", +}; export const VALUE_KEY = b64encode("value"); export const STORE_KEY = b64encode("store"); @@ -31,3 +35,16 @@ export const KEY_KEY = b64encode("key"); export const STORE_NAME_KEY = b64encode("store_name"); export const SUBKEY_KEY = b64encode("store_subkey"); export const UNPROVED_VALUE_KEY = b64encode("unproved_value"); +export const PACKET_DATA_KEY = "packet_data"; +export const PACKET_SRC_CHANNEL_KEY = "packet_src_channel"; +export const PACKET_DST_CHANNEL_KEY = "packet_dst_channel"; +export const PACKET_SRC_PORT_KEY = "packet_src_port"; +export const PACKET_DST_PORT_KEY = "packet_dst_port"; +export const ACTION_KEY = b64encode("action"); +export const IBC_MESSAGE_TRANSFER_VALUE = b64encode("/ibc.applications.transfer.v1.MsgTransfer"); +export const IBC_MESSAGE_RECEIVE_PACKET_VALUE = b64encode("/ibc.core.channel.v1.MsgRecvPacket"); +export const RECEPIENT_KEY = b64encode("recipient"); +export const SENDER_KEY = b64encode("sender"); +export const RECEIVER_KEY = b64encode("receiver"); +export const AMOUNT_KEY = b64encode("amount"); +export const TRANSFER_PORT_VALUE = "transfer"; diff --git a/src/mappings/mappingHandlers.ts b/src/mappings/mappingHandlers.ts index 0c1efba6..d383490d 100644 --- a/src/mappings/mappingHandlers.ts +++ b/src/mappings/mappingHandlers.ts @@ -1,5 +1,8 @@ import { StateChangeEvent, + IBCChannel, + IBCTransfer, + TransferType, } from "../types"; import { CosmosEvent } from "@subql/types-cosmos"; import { @@ -7,6 +10,7 @@ import { extractStoragePath, getStateChangeModule, resolveBrandNamesAndValues, + getEscrowAddress, } from "./utils"; import { @@ -18,6 +22,12 @@ import { STORE_NAME_KEY, SUBKEY_KEY, UNPROVED_VALUE_KEY, + PACKET_DATA_KEY, + PACKET_SRC_CHANNEL_KEY, + PACKET_DST_CHANNEL_KEY, + PACKET_DST_PORT_KEY, + PACKET_SRC_PORT_KEY, + TRANSFER_PORT_VALUE, } from "./constants"; import { psmEventKit } from "./events/psm"; import { boardAuxEventKit } from "./events/boardAux"; @@ -30,6 +40,98 @@ BigInt.prototype.toJSON = function () { return this.toString(); }; +async function saveIbcChannel(channelName: string) { + const generatedEscrowAddress = getEscrowAddress(TRANSFER_PORT_VALUE, channelName); + + const channelRecord = new IBCChannel(channelName, channelName, generatedEscrowAddress); + await channelRecord.save(); +} + +export async function handleIbcSendPacketEvent(cosmosEvent: CosmosEvent): Promise { + const { event, block, tx } = cosmosEvent; + if (event.type != EVENT_TYPES.SEND_PACKET) { + logger.warn("Not valid send_packet event."); + return; + } + + const packetSrcPortAttr = event.attributes.find((a) => a.key === PACKET_SRC_PORT_KEY); + if (!packetSrcPortAttr || packetSrcPortAttr.value !== TRANSFER_PORT_VALUE) { + logger.warn("packet_src_port is not transfer"); + return; + } + const packetDataAttr = event.attributes.find((a) => a.key === PACKET_DATA_KEY); + if (!packetDataAttr) { + logger.warn("No packet data attribute found"); + return; + } + + const packetSrcChannelAttr = event.attributes.find((a) => a.key === PACKET_SRC_CHANNEL_KEY); + if (!packetSrcChannelAttr) { + logger.warn("No packet source channel found"); + return; + } + const { amount, denom, receiver, sender } = JSON.parse(packetDataAttr.value); + const sourceChannel = packetSrcChannelAttr.value; + + await saveIbcChannel(sourceChannel); + + const transferRecord = new IBCTransfer( + tx.hash, + block.header.time as any, + BigInt(block.header.height), + packetSrcChannelAttr.value, + sender, + receiver, + denom, + amount, + TransferType.SEND, + ); + await transferRecord.save(); +} + +export async function handleIbcReceivePacketEvent(cosmosEvent: CosmosEvent): Promise { + const { event, block, tx } = cosmosEvent; + if (event.type != EVENT_TYPES.RECEIVE_PACKET) { + logger.warn("Not valid recv_packet event."); + return; + } + + const packetDataAttr = event.attributes.find((a) => a.key === PACKET_DATA_KEY); + if (!packetDataAttr) { + logger.warn("No packet data attribute found"); + return; + } + + const packetDestPortAttr = event.attributes.find((a) => a.key === PACKET_DST_PORT_KEY); + if (!packetDestPortAttr || packetDestPortAttr.value !== TRANSFER_PORT_VALUE) { + logger.warn("packet_dest_port is not transfer"); + return; + } + + const packetDstChannelAttr = event.attributes.find((a) => a.key === PACKET_DST_CHANNEL_KEY); + if (!packetDstChannelAttr) { + logger.warn("No packet destination channel found"); + return; + } + const { amount, denom, receiver, sender } = JSON.parse(packetDataAttr.value); + const destinationChannel = packetDstChannelAttr.value; + + await saveIbcChannel(destinationChannel); + + const transferRecord = new IBCTransfer( + tx.hash, + block.header.time as any, + BigInt(block.header.height), + destinationChannel, + sender, + receiver, + denom, + amount, + TransferType.RECEIVE, + ); + await transferRecord.save(); +} + export async function handleStateChangeEvent(cosmosEvent: CosmosEvent): Promise { const { event, block } = cosmosEvent; diff --git a/src/mappings/utils.ts b/src/mappings/utils.ts index 07c1d8c6..53b62faf 100644 --- a/src/mappings/utils.ts +++ b/src/mappings/utils.ts @@ -1,3 +1,6 @@ +import { bech32 } from "bech32"; +import sha256 from "js-sha256"; + export function extractBrand(str: string): string { return str.replace("Alleged: ", "").replace(" brand", ""); } @@ -95,3 +98,13 @@ export function dateToDayKey(timestamp: any): number { const day = date.getUTCDate().toString().padStart(2, "0"); return parseInt(`${year}${month}${day}`); } + +export const getEscrowAddress = (port: string, channel: string) => { + const version = "ics20-1"; + const chainPrefix = "agoric"; + const stringtoSha = Buffer.from([...Buffer.from(version), 0, ...Buffer.from(`${port}/${channel}`)]); + const shaHash = sha256.sha256.array(stringtoSha.toString()); + const bechWords = bech32.toWords(shaHash.slice(0, 20)); + const address = bech32.encode(chainPrefix, bechWords); + return address; +};