diff --git a/.changeset/three-moles-reflect.md b/.changeset/three-moles-reflect.md new file mode 100644 index 0000000..8c16d73 --- /dev/null +++ b/.changeset/three-moles-reflect.md @@ -0,0 +1,6 @@ +--- +"@solanafm/explorer-kit": patch +"@solanafm/explorer-kit-idls": patch +--- + +support shank event parsing diff --git a/packages/explorerkit-idls/package.json b/packages/explorerkit-idls/package.json index d4b6986..f237654 100644 --- a/packages/explorerkit-idls/package.json +++ b/packages/explorerkit-idls/package.json @@ -36,7 +36,7 @@ "dependencies": { "@coral-xyz/anchor": "^0.29.0", "@coral-xyz/anchor-new": "npm:@coral-xyz/anchor@^0.30.0", - "@solanafm/kinobi-lite": "^0.12.0", + "@solanafm/kinobi-lite": "^0.12.3", "axios": "^1.3.3" } } diff --git a/packages/explorerkit-translator/package.json b/packages/explorerkit-translator/package.json index 3174d65..4a55d21 100644 --- a/packages/explorerkit-translator/package.json +++ b/packages/explorerkit-translator/package.json @@ -51,7 +51,7 @@ "@metaplex-foundation/umi": "^0.8.6", "@metaplex-foundation/umi-serializers": "^0.8.5", "@solana/spl-type-length-value": "^0.1.0", - "@solanafm/kinobi-lite": "^0.12.0", + "@solanafm/kinobi-lite": "^0.12.3", "@solanafm/utils": "^1.0.5" } } diff --git a/packages/explorerkit-translator/src/helpers/KinobiTreeGenerator.ts b/packages/explorerkit-translator/src/helpers/KinobiTreeGenerator.ts index e80fe9c..a01f1eb 100644 --- a/packages/explorerkit-translator/src/helpers/KinobiTreeGenerator.ts +++ b/packages/explorerkit-translator/src/helpers/KinobiTreeGenerator.ts @@ -38,6 +38,7 @@ import { EnumVariantTypeNode, getAllAccounts, getAllDefinedTypes, + getAllEvents, getAllInstructions, Idl, isStructTypeNode, @@ -322,6 +323,30 @@ export class KinobiTreeGenerator { return typeLayout; + case KinobiTreeGeneratorType.EVENTS: + const eventNodes = getAllEvents(this.rootNode); + const eventsLayout = new Map(); + + eventNodes.forEach((eventNode, index) => { + if (isStructTypeNode(eventNode.dataArgs.struct)) { + const serializer = KinobiTreeGenerator.createSerializer( + eventNode.dataArgs.struct, + typeNodes, + eventNode.dataArgs.name, + treeGeneratorType + ); + const fmShankSerializer: FMShankSerializer = { + serializer: serializer[1], + instructionName: serializer[0], + }; + + // TODO: Will try to strongly take reference from the IDL in the future + const eventDiscriminator = index; + eventsLayout.set(eventDiscriminator, fmShankSerializer); + } + }); + + return eventsLayout; default: return new Map(); } diff --git a/packages/explorerkit-translator/src/interfaces/EventParserInterface.ts b/packages/explorerkit-translator/src/interfaces/EventParserInterface.ts index aae0ab2..eea7ef0 100644 --- a/packages/explorerkit-translator/src/interfaces/EventParserInterface.ts +++ b/packages/explorerkit-translator/src/interfaces/EventParserInterface.ts @@ -1,6 +1,6 @@ import { BorshEventCoder, BorshInstructionCoder } from "@coral-xyz/anchor"; -import { createAnchorEventParser } from "../parsers/v2/event"; +import { createAnchorEventParser, createShankEventParser } from "../parsers/v2/event"; import { createBubblegumEventParser } from "../parsers/v2/event/anchor/bubblegum"; import { createSPLCompEventParser } from "../parsers/v2/event/anchor/spl-compression"; import { createTCompEventParser } from "../parsers/v2/event/anchor/tcomp"; @@ -42,6 +42,12 @@ export const createEventParser = (idlItem: IdlItem, programHash: string) => { return null; } + case "shank": + switch (programHash) { + default: + return createShankEventParser(idlItem); + } + default: return null; } diff --git a/packages/explorerkit-translator/src/parsers/v2/event/index.ts b/packages/explorerkit-translator/src/parsers/v2/event/index.ts index 28f18ca..d144310 100644 --- a/packages/explorerkit-translator/src/parsers/v2/event/index.ts +++ b/packages/explorerkit-translator/src/parsers/v2/event/index.ts @@ -1 +1,2 @@ export { createAnchorEventParser } from "./anchor"; +export { createShankEventParser } from "./shank"; diff --git a/packages/explorerkit-translator/src/parsers/v2/event/shank.ts b/packages/explorerkit-translator/src/parsers/v2/event/shank.ts new file mode 100644 index 0000000..10e2c74 --- /dev/null +++ b/packages/explorerkit-translator/src/parsers/v2/event/shank.ts @@ -0,0 +1,60 @@ +import { Idl } from "@solanafm/kinobi-lite"; +import { convertBNToNumberInObject } from "@solanafm/utils"; + +import { EventParserInterface, ParserOutput, ParserType } from "../../.."; +import { mapDataTypeToName } from "../../../helpers/idl"; +import { KinobiTreeGenerator } from "../../../helpers/KinobiTreeGenerator"; +import { IdlItem } from "../../../types/IdlItem"; +import { FMShankSerializer, KinobiTreeGeneratorType } from "../../../types/KinobiTreeGenerator"; + +export const createShankEventParser: (idlItem: IdlItem) => EventParserInterface = (idlItem: IdlItem) => { + const idl = idlItem.idl as Idl; + const eventsLayout = new KinobiTreeGenerator(idl).constructLayout(KinobiTreeGeneratorType.EVENTS); + + const parseEvents = (eventData: string, mapTypes?: boolean): ParserOutput => { + try { + if (eventsLayout) { + let dataBuffer: Buffer = Buffer.from(eventData, "base64"); + let eventSerializer: FMShankSerializer | undefined = undefined; + if (dataBuffer.byteLength > 0) { + // Let's assume all the events have enums as u8 for now, if we need to support other discriminants we will need to add them from Kinobi + const borshDiscriminant = Buffer.from(dataBuffer).readUint8(0); + eventSerializer = eventsLayout.get(borshDiscriminant); + } + + if (eventSerializer) { + const decodedEventData = eventSerializer.serializer?.deserialize(dataBuffer); + + if (decodedEventData && decodedEventData[0]) { + const filteredIdlEvent = + idl.events?.filter( + (event) => event.name.toLowerCase() === eventSerializer.instructionName.toLowerCase() + ) ?? []; + + if (mapTypes) { + decodedEventData[0] = mapDataTypeToName(decodedEventData[0], filteredIdlEvent[0]?.fields); + } + + decodedEventData[0] = convertBNToNumberInObject(decodedEventData[0]); + + return { + name: eventSerializer.instructionName, + data: convertBNToNumberInObject(decodedEventData[0]), + type: ParserType.EVENT, + }; + } + } + } + + return null; + } catch (error) { + console.error(error); + return null; + } + }; + + return { + eventsLayout, + parseEvents, + }; +}; diff --git a/packages/explorerkit-translator/src/types/KinobiTreeGenerator.ts b/packages/explorerkit-translator/src/types/KinobiTreeGenerator.ts index f22c252..9644349 100644 --- a/packages/explorerkit-translator/src/types/KinobiTreeGenerator.ts +++ b/packages/explorerkit-translator/src/types/KinobiTreeGenerator.ts @@ -15,4 +15,5 @@ export enum KinobiTreeGeneratorType { ACCOUNTS, INSTRUCTIONS, TYPES, + EVENTS, } diff --git a/packages/explorerkit-translator/tests/v2/event.test.ts b/packages/explorerkit-translator/tests/v2/event.test.ts index 77687c6..0c9f670 100644 --- a/packages/explorerkit-translator/tests/v2/event.test.ts +++ b/packages/explorerkit-translator/tests/v2/event.test.ts @@ -59,3 +59,57 @@ describe("createAnchorEventParser", () => { } }); }); + +describe("createShankEventParser", () => { + it("should construct an shank event parser for a given valid IDL", async () => { + const programId = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; + const idlItem = await getProgramIdl(programId); + + if (idlItem) { + const parser = new SolanaFMParser(idlItem, programId); + const eventParser = parser.createParser(ParserType.EVENT); + + expect(eventParser).not.toBeNull(); + expect(checkIfInstructionParser(eventParser)).toBe(false); + expect(checkIfEventParser(eventParser)).toBe(true); + } + }); + + it("should construct an anchor event parser for a given valid IDL and parses the event data", async () => { + const programId = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; + const eventData = "A8Ce5gUAAAAAnhJhfQsAAAACAAAAAAAAAMCe5gUAAAAAeU9gpSYAAAD7ESaLIGIAAJn+yu8OAAAA"; + const idlItem = await getProgramIdl(programId); + + if (idlItem) { + const parser = new SolanaFMParser(idlItem, programId); + const eventParser = parser.createParser(ParserType.EVENT); + + if (eventParser && checkIfEventParser(eventParser)) { + const decodedData = eventParser.parseEvents(eventData); + expect(decodedData).not.toBeNull(); + expect(decodedData?.type).toBe("event"); + expect(decodedData?.name).toBe("swapBaseIn"); + } + } + }); + + it("should construct an anchor event parser for a given valid IDL and parses the event data and properly map the data type with the given IDL", async () => { + const programId = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; + const eventData = "A8Ce5gUAAAAAnhJhfQsAAAACAAAAAAAAAMCe5gUAAAAAeU9gpSYAAAD7ESaLIGIAAJn+yu8OAAAA"; + + const idlItem = await getProgramIdl(programId); + + if (idlItem) { + const parser = new SolanaFMParser(idlItem, programId); + const eventParser = parser.createParser(ParserType.EVENT); + + if (eventParser && checkIfEventParser(eventParser)) { + const decodedData = eventParser.parseEvents(eventData, true); + expect(decodedData).not.toBeNull(); + expect(decodedData?.type).toBe("event"); + expect(decodedData?.name).toBe("swapBaseIn"); + expect(decodedData?.data["minimumAmountOut"].type).toBe("u64"); + } + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe76aab..1dfda8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: npm:@coral-xyz/anchor@^0.30.0 version: /@coral-xyz/anchor@0.30.0 '@solanafm/kinobi-lite': - specifier: ^0.12.0 - version: 0.12.0 + specifier: ^0.12.3 + version: 0.12.3 axios: specifier: ^1.3.3 version: 1.3.3 @@ -152,8 +152,8 @@ importers: specifier: ^0.1.0 version: 0.1.0 '@solanafm/kinobi-lite': - specifier: ^0.12.0 - version: 0.12.0 + specifier: ^0.12.3 + version: 0.12.3 '@solanafm/utils': specifier: ^1.0.5 version: 1.0.5 @@ -226,8 +226,8 @@ packages: regenerator-runtime: 0.14.1 dev: false - /@babel/runtime@7.24.5: - resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} + /@babel/runtime@7.24.6: + resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 @@ -449,9 +449,9 @@ packages: resolution: {integrity: sha512-qreDh5ztiRHVnCbJ+RS70NJ6aSTPBYDAgFeQ7Z5QvaT5DcDIhNyt4onOciVz2ieIE1XWePOJDDu9SbNvPGBkvQ==} engines: {node: '>=11'} dependencies: - '@coral-xyz/borsh': 0.30.0(@solana/web3.js@1.91.7) + '@coral-xyz/borsh': 0.30.0(@solana/web3.js@1.91.8) '@noble/hashes': 1.4.0 - '@solana/web3.js': 1.91.7 + '@solana/web3.js': 1.91.8 bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -480,13 +480,13 @@ packages: buffer-layout: 1.2.2 dev: false - /@coral-xyz/borsh@0.30.0(@solana/web3.js@1.91.7): + /@coral-xyz/borsh@0.30.0(@solana/web3.js@1.91.8): resolution: {integrity: sha512-OrcV+7N10cChhgDRUxM4iEIuwxUHHs52XD85R8cFCUqE0vbLYrcoPPPs+VF6kZ9DhdJGVW2I6DHJOp5TykyZog==} engines: {node: '>=10'} peerDependencies: '@solana/web3.js': ^1.68.0 dependencies: - '@solana/web3.js': 1.91.7 + '@solana/web3.js': 1.91.8 bn.js: 5.2.1 buffer-layout: 1.2.2 dev: false @@ -1358,10 +1358,10 @@ packages: - utf-8-validate dev: false - /@solana/web3.js@1.91.7: - resolution: {integrity: sha512-HqljZKDwk6Z4TajKRGhGLlRsbGK4S8EY27DA7v1z6yakewiUY3J7ZKDZRxcqz2MYV/ZXRrJ6wnnpiHFkPdv0WA==} + /@solana/web3.js@1.91.8: + resolution: {integrity: sha512-USa6OS1jbh8zOapRJ/CBZImZ8Xb7AJjROZl5adql9TpOoBN9BUzyyouS5oPuZHft7S7eB8uJPuXWYjMi6BHgOw==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.24.6 '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 @@ -1374,7 +1374,7 @@ packages: fast-stable-stringify: 1.0.0 jayson: 4.1.0 node-fetch: 2.7.0 - rpc-websockets: 7.10.0 + rpc-websockets: 7.11.0 superstruct: 0.14.2 transitivePeerDependencies: - bufferutil @@ -1382,10 +1382,10 @@ packages: - utf-8-validate dev: false - /@solanafm/kinobi-lite@0.12.0: - resolution: {integrity: sha512-K3daAv8HoJzM6UiDUG1Tj0tZIcCSJENmqx5nw4J2+sOwyZm1leFvVkIHM+WOExh/IqI7PglipRJbrF0OXN007Q==} + /@solanafm/kinobi-lite@0.12.3: + resolution: {integrity: sha512-1a5edBWhDSuVRqJw/A3VCDU1W20of4FpEBc/B1/U7KFssxZLwq5A16DYZMMI5bM0hl9etPV0x+cz2m9cOWeVqQ==} dependencies: - '@noble/hashes': 1.3.2 + '@noble/hashes': 1.4.0 chalk: 4.1.2 dev: false @@ -4684,6 +4684,17 @@ packages: utf-8-validate: 5.0.10 dev: false + /rpc-websockets@7.11.0: + resolution: {integrity: sha512-IkLYjayPv6Io8C/TdCL5gwgzd1hFz2vmBZrjMw/SPEXo51ETOhnzgS4Qy5GWi2JQN7HKHa66J3+2mv0fgNh/7w==} + dependencies: + eventemitter3: 4.0.7 + uuid: 8.3.2 + ws: 8.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + dev: false + /rpc-websockets@7.6.1: resolution: {integrity: sha512-MmRGaJJvxTHSRxYPjJJqcj2zWnCetw7YbYbKlD0Yc7qVw6PsZhRJg1MI3mpWlpBs+4zO+urlNfLl9zLsdOD/gA==} dependencies: @@ -5909,6 +5920,22 @@ packages: utf-8-validate: 5.0.10 dev: false + /ws@8.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + dev: false + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: true