Skip to content

Commit da14a89

Browse files
authored
Support decoding codama program events (#25)
* Support decoding codama program events * Update changelogs * Tidy up
1 parent 9673463 commit da14a89

File tree

6 files changed

+2332
-6
lines changed

6 files changed

+2332
-6
lines changed

packages/common-solana/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Util function to get discriminator from name (#25)
810

911
## [1.1.1] - 2025-09-10
1012
### Fixed

packages/common-solana/src/codegen/idl.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors
22
// SPDX-License-Identifier: GPL-3.0
33

4+
import {createHash} from 'node:crypto';
45
import fs from 'node:fs';
56
import {AnchorIdl, IdlV01, rootNodeFromAnchor} from '@codama/nodes-from-anchor';
67
import {getBase16Encoder, getBase58Encoder, getBase64Encoder, getUtf8Encoder} from '@solana/codecs-strings';
@@ -87,3 +88,9 @@ export function findInstructionDiscriminatorByName(rootNode: RootNode, name: str
8788
return undefined;
8889
}
8990
}
91+
92+
// Discriminator are the first 8 bytes of the sha256 over the event's name
93+
export function getDiscriminator(name: string): Uint8Array {
94+
const hash = createHash('sha256').update(`event:${name}`).digest();
95+
return new Uint8Array(hash.subarray(0, 8));
96+
}

packages/node/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Support for decoding codama program logs (#25)
810

911
## [6.1.2] - 2025-09-11
1012
### Fixed

packages/node/src/solana/decoder.spec.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import { SolanaDecoder } from './decoder';
1111
import { getProgramId, filterInstructionsProcessor } from './utils.solana';
1212

1313
const HTTP_ENDPOINT =
14-
process.env.HTTP_ENDPOINT ?? 'https://solana.api.onfinality.io/public';
14+
process.env.HTTP_ENDPOINT ??
15+
'https://api.mainnet-beta.solana.com' ??
16+
'https://solana.api.onfinality.io/public';
1517

18+
const IDL_codama_0_1_0: IdlV01 = require('../../test/8t2R21V3vjS1ucZzmX2memtGptjYZi2yGY3cYVa8dak7.idl.json');
1619
const IDL_Jupiter: IdlV01 = require('../../test/JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4.idl.json');
1720
const IDL_swap: IdlV01 = require('../../test/swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW.idl.json');
1821
const IDL_token: RootNode = require('../../test/TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.idl.json');
@@ -47,6 +50,9 @@ describe('SolanaDecoder', () => {
4750
decoder.idls['swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW'] = IDL_swap;
4851
// eslint-disable-next-line @typescript-eslint/dot-notation
4952
decoder.idls['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'] = IDL_token;
53+
// eslint-disable-next-line @typescript-eslint/dot-notation
54+
decoder.idls['8t2R21V3vjS1ucZzmX2memtGptjYZi2yGY3cYVa8dak7'] =
55+
IDL_codama_0_1_0;
5056
};
5157

5258
describe('caching IDLs', () => {
@@ -265,5 +271,43 @@ describe('SolanaDecoder', () => {
265271
expect(decoded!.name).toBe('poolBalanceUpdatedEvent');
266272
expect(decoded!.data).toEqual(logData);
267273
});
274+
275+
it('can decode codama 0.1.0 spec idl events', async () => {
276+
const solanaApi = await SolanaApi.create(
277+
'https://api.devnet.solana.com',
278+
new EventEmitter2(),
279+
decoder,
280+
);
281+
282+
const block = await solanaApi.fetchBlock(405402294);
283+
284+
const tx = findTx(
285+
block.block,
286+
'4xHLJtomvuF2DAwtDoHgGqABv47kmqMCvd691bRe9XhiAP99grjvpAdvNQGnA4PETZSvhXdfHvvHUNWu2SNM3aqE',
287+
);
288+
289+
expect(tx).toBeDefined();
290+
291+
const programLogs = tx!.meta!.logs?.filter((l) =>
292+
l.message.startsWith('Program data:'),
293+
);
294+
expect(programLogs).toBeDefined();
295+
const decoded = await programLogs![0].decodedMessage;
296+
expect(decoded).not.toBeNull();
297+
expect(decoded!.name).toEqual('createCampaign');
298+
expect(decoded!.data).toMatchObject({
299+
aggregateAmount: 10000n,
300+
campaign: 'BjYpCVaiksvD8Dw4LixUJVNaCptefKk86nDtw419b7Y5',
301+
campaignName: 'HODL or Nothing',
302+
campaignStartTime: 1754142441n,
303+
creator: 'HTtnrJ5iq9HVVypJZxFKMCcR6JDiUqT6yaE7c6BvfeTp',
304+
expirationTime: 1757776469n,
305+
ipfsCid: 'bafkreiecpwdhvkmw4y6iihfndk7jhwjas3m5htm7nczovt6m37mucwgsrq',
306+
merkleRoot: ['base64', '1SVJywcqH80FJBL8gPZ47/6Sru7czRyuYyxcbh3ok3k='],
307+
recipientCount: 100,
308+
tokenDecimals: 6,
309+
tokenMint: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
310+
});
311+
});
268312
});
269313
});

packages/node/src/solana/decoder.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
isAnchorIdlV01,
1111
getInstructionDiscriminatorBytes,
1212
findInstructionDiscriminatorByName,
13+
getDiscriminator,
14+
isRootNode,
15+
isAnchorIdl,
1316
} from '@subql/common-solana';
1417
import { getLogger } from '@subql/node-core';
1518
import {
@@ -21,7 +24,14 @@ import {
2124
} from '@subql/types-solana';
2225
import { isHex } from '@subql/utils';
2326
import bs58 from 'bs58';
24-
import { camelCase, DefinedTypeNode, InstructionNode, RootNode } from 'codama';
27+
import {
28+
camelCase,
29+
DefinedTypeNode,
30+
InstructionNode,
31+
pascalCase,
32+
RootNode,
33+
titleCase,
34+
} from 'codama';
2535
import { Memoize } from '../utils/decorators';
2636
import { allAccounts, getProgramId } from './utils.solana';
2737

@@ -99,14 +109,36 @@ export function decodeInstruction(
99109

100110
// Codama doesn't support Logs so extra work is required to decode logs
101111
export function decodeLog(idl: Idl, message: string): DecodedData | null {
112+
const msgData = message.replace('Program data: ', '');
113+
const msgBuffer = basedToBuffer(msgData);
114+
115+
// Codama IDL, doesn't include events in the IDL but it should decoded to a definedType in the IDL
116+
if (!isAnchorIdl(idl)) {
117+
if (msgBuffer.length < 8) {
118+
// Not enough data for a discriminator
119+
return null;
120+
}
121+
122+
// Split the discriminator and data
123+
const logDisc = msgBuffer.subarray(0, 8);
124+
const data = msgBuffer.subarray(8);
125+
126+
// Attempt to find the matching type by discriminator and decode the data
127+
return decodeData(idl, data.toString('base64'), (root) => {
128+
return root.program.definedTypes.find(
129+
(t) =>
130+
// Warning this is fragile as there can be various casings of the type names, but because it requires hashing data we can only go one way
131+
logDisc.indexOf(getDiscriminator(t.name)) === 0 ||
132+
logDisc.indexOf(getDiscriminator(pascalCase(t.name))) === 0,
133+
);
134+
});
135+
}
136+
102137
// Older versions don't support events
103138
if (!isAnchorIdlV01(idl)) {
104139
throw new Error('Only Anchor IDL v0.1.0 is supported for decoding logs');
105140
}
106141

107-
const msgData = message.replace('Program data: ', '') as any;
108-
const msgBuffer = basedToBuffer(msgData);
109-
110142
// Codama doesn't include events so we have to find it manually
111143
const event = idl.events?.find(
112144
(e) => msgBuffer.indexOf(Buffer.from(e.discriminator)) === 0,
@@ -121,7 +153,7 @@ export function decodeLog(idl: Idl, message: string): DecodedData | null {
121153
.subarray(event.discriminator.length)
122154
.toString('base64');
123155

124-
return decodeData(idl, input as any, (root, data) => {
156+
return decodeData(idl, input, (root) => {
125157
return root.program.definedTypes.find(
126158
(t) => t.name === event.name || t.name === camelCase(event.name),
127159
);

0 commit comments

Comments
 (0)