diff --git a/tests/events/tests/events.ts b/tests/events/tests/events.ts index 7cd2333a78..5704ef0770 100644 --- a/tests/events/tests/events.ts +++ b/tests/events/tests/events.ts @@ -58,13 +58,27 @@ describe("Events", () => { const ixData = anchor.utils.bytes.bs58.decode( txResult.meta.innerInstructions[0].instructions[0].data ); + const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); const event = program.coder.events.decode(eventData); + assertCpiEvent(event); + + // ensure the coder works directly with cpiEvent + const cpiEvent = program.coder.events.decode( + txResult.meta.innerInstructions[0].instructions[0].data + ); + assertCpiEvent(cpiEvent); + const cpiEvents = program.coder.events.parseCpiEvents(txResult); + assert(cpiEvents.length === 1); + assertCpiEvent(cpiEvents[0]); + }); + + function assertCpiEvent(event: any) { assert.strictEqual(event.name, "myOtherEvent"); assert.strictEqual(event.data.label, "cpi"); assert.strictEqual((event.data.data as anchor.BN).toNumber(), 7); - }); + } it("Throws on unauthorized invocation", async () => { const tx = new anchor.web3.Transaction(); diff --git a/ts/packages/anchor/src/coder/borsh/event.ts b/ts/packages/anchor/src/coder/borsh/event.ts index 0ce5c0971e..586d7131e6 100644 --- a/ts/packages/anchor/src/coder/borsh/event.ts +++ b/ts/packages/anchor/src/coder/borsh/event.ts @@ -1,11 +1,28 @@ import { Buffer } from "buffer"; import { Layout } from "buffer-layout"; import * as base64 from "../../utils/bytes/base64.js"; +import * as bs58 from "../../utils/bytes/bs58.js"; import { Idl } from "../../idl.js"; import { IdlCoder } from "./idl.js"; import { EventCoder } from "../index.js"; +import BN from "bn.js"; +import { + CompiledInnerInstruction, + PublicKey, + Transaction, + TransactionResponse, + VersionedTransactionResponse, +} from "@solana/web3.js"; export class BorshEventCoder implements EventCoder { + /** + * CPI event discriminator. + * https://github.com/coral-xyz/anchor/blob/v0.29.0/lang/src/event.rs + */ + private static eventIxTag: BN = new BN("1d9acb512ea545e4", "hex"); + + public address: PublicKey; + /** * Maps account type identifier to a layout. */ @@ -42,12 +59,26 @@ export class BorshEventCoder implements EventCoder { ev.name, ]) ); + + this.address = new PublicKey(idl.address); } + /** + * + * @param log base 64 encoded log data from transaction message + * or base58 encoded transaction message or CPI encoded event data + * @returns decoded event object or null + */ public decode(log: string): { name: string; data: any; } | null { + const transactionCpiData = this.parseAsTransactionCpiData(log); + if (transactionCpiData !== null) { + // log parsed to be CPI data, recursive call stripped event data + return this.decode(transactionCpiData); + } + let logArr: Buffer; // This will throw if log length is not a multiple of 4. try { @@ -55,9 +86,9 @@ export class BorshEventCoder implements EventCoder { } catch (e) { return null; } - const disc = base64.encode(logArr.slice(0, 8)); // Only deserialize if the discriminator implies a proper event. + const disc = base64.encode(logArr.slice(0, 8)); const eventName = this.discriminators.get(disc); if (!eventName) { return null; @@ -70,4 +101,59 @@ export class BorshEventCoder implements EventCoder { const data = layout.decode(logArr.slice(8)); return { data, name: eventName }; } + + /** + * Check the log data to be transaction CPI event: + * Expected data format: + * < cpi event discriminator | event name discriminator | event data > + * If matches cpi event discriminator + * < event name | event data> base64 formatted is returned + * otherwise null is returned. + */ + parseAsTransactionCpiData(log: string): string | null { + let encodedLog: Buffer; + try { + // verification if log is transaction cpi data encoded with base58 + encodedLog = bs58.decode(log); + } catch (e) { + return null; + } + const disc = encodedLog.slice(0, 8); + if (disc.equals(BorshEventCoder.eventIxTag.toBuffer("le"))) { + // after CPI tag data follows in format of standard event + return base64.encode(encodedLog.slice(8)); + } else { + return null; + } + } + + public parseCpiEvents( + transactionResponse: VersionedTransactionResponse | TransactionResponse + ): { name: string; data: any }[] { + const events: { name: string; data: any }[] = []; + const inner: CompiledInnerInstruction[] = + transactionResponse?.meta?.innerInstructions ?? []; + const idlProgramId = this.address; + for (let i = 0; i < inner.length; i++) { + for (let j = 0; j < inner[i].instructions.length; j++) { + const ix = inner[i].instructions[j]; + const programPubkey = + transactionResponse?.transaction.message.staticAccountKeys[ + ix.programIdIndex + ]; + if ( + programPubkey === undefined || + !programPubkey.equals(idlProgramId) + ) { + // we are at instructions that does not match the linked program + continue; + } + const event = this.decode(ix.data); + if (event) { + events.push(event); + } + } + } + return events; + } } diff --git a/ts/packages/anchor/src/coder/index.ts b/ts/packages/anchor/src/coder/index.ts index 6d0d482d64..3a762b0b9d 100644 --- a/ts/packages/anchor/src/coder/index.ts +++ b/ts/packages/anchor/src/coder/index.ts @@ -1,3 +1,4 @@ +import { VersionedTransactionResponse } from "@solana/web3.js"; import { IdlEvent } from "../idl.js"; import { Event } from "../program/event.js"; @@ -50,6 +51,24 @@ export interface EventCoder { decode>( log: string ): Event | null; + + parseCpiEvents>( + transactionResponse: VersionedTransactionResponse + ): Event[]; +} + +export abstract class NoEventCoder implements EventCoder { + decode>( + _log: string + ): Event | null { + throw new Error(`${this.constructor} program does not have events`); + } + + parseCpiEvents>( + _transactionResponse: VersionedTransactionResponse + ): Event[] { + throw new Error(`${this.constructor} program does not have CPI events`); + } } export interface TypesCoder { diff --git a/ts/packages/anchor/src/coder/system/events.ts b/ts/packages/anchor/src/coder/system/events.ts index 9a9f5faac9..8ddd7e0711 100644 --- a/ts/packages/anchor/src/coder/system/events.ts +++ b/ts/packages/anchor/src/coder/system/events.ts @@ -2,6 +2,7 @@ import { EventCoder } from "../index.js"; import { Idl } from "../../idl.js"; import { Event } from "../../program/event"; import { IdlEvent } from "../../idl"; +import { VersionedTransactionResponse } from "@solana/web3.js"; export class SystemEventsCoder implements EventCoder { constructor(_idl: Idl) {} @@ -9,6 +10,11 @@ export class SystemEventsCoder implements EventCoder { decode>( _log: string ): Event | null { - throw new Error("System program does not have events"); + throw new Error("SystemProgram does not have events."); + } + parseCpiEvents>( + _transactionResponse: VersionedTransactionResponse + ): Event[] { + throw new Error("SystemProgram does not have CPI events."); } } diff --git a/ts/packages/spl-associated-token-account/src/coder/events.ts b/ts/packages/spl-associated-token-account/src/coder/events.ts index 28b927101f..c74bdab64c 100644 --- a/ts/packages/spl-associated-token-account/src/coder/events.ts +++ b/ts/packages/spl-associated-token-account/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplAssociatedTokenAccountEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplAssociatedTokenAccount program does not have events"); +export class SplAssociatedTokenAccountEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-binary-option/src/coder/events.ts b/ts/packages/spl-binary-option/src/coder/events.ts index 91261a59cc..d5862a4955 100644 --- a/ts/packages/spl-binary-option/src/coder/events.ts +++ b/ts/packages/spl-binary-option/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplBinaryOptionEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplBinaryOption program does not have events"); +export class SplBinaryOptionEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-binary-oracle-pair/src/coder/events.ts b/ts/packages/spl-binary-oracle-pair/src/coder/events.ts index 173360b9da..981726ad34 100644 --- a/ts/packages/spl-binary-oracle-pair/src/coder/events.ts +++ b/ts/packages/spl-binary-oracle-pair/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplBinaryOraclePairEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplBinaryOraclePair program does not have events"); +export class SplBinaryOraclePairEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-feature-proposal/src/coder/events.ts b/ts/packages/spl-feature-proposal/src/coder/events.ts index b45acf4eb6..8e16d8389c 100644 --- a/ts/packages/spl-feature-proposal/src/coder/events.ts +++ b/ts/packages/spl-feature-proposal/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplFeatureProposalEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplFeatureProposal program does not have events"); +export class SplFeatureProposalEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-governance/src/coder/events.ts b/ts/packages/spl-governance/src/coder/events.ts index b31ea4c441..c9de383bc3 100644 --- a/ts/packages/spl-governance/src/coder/events.ts +++ b/ts/packages/spl-governance/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplGovernanceEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplGovernance program does not have events"); +export class SplGovernanceEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-memo/src/coder/events.ts b/ts/packages/spl-memo/src/coder/events.ts index 061a6ad017..8a13c3e7a7 100644 --- a/ts/packages/spl-memo/src/coder/events.ts +++ b/ts/packages/spl-memo/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplMemoEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplMemo program does not have events"); +export class SplMemoEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-name-service/src/coder/events.ts b/ts/packages/spl-name-service/src/coder/events.ts index d130ba977f..fa9c7a9683 100644 --- a/ts/packages/spl-name-service/src/coder/events.ts +++ b/ts/packages/spl-name-service/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplNameServiceEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplNameService program does not have events"); +export class SplNameServiceEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-record/src/coder/events.ts b/ts/packages/spl-record/src/coder/events.ts index 88004d32db..f32fde9b1c 100644 --- a/ts/packages/spl-record/src/coder/events.ts +++ b/ts/packages/spl-record/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplRecordEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplRecord program does not have events"); +export class SplRecordEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-stake-pool/src/coder/events.ts b/ts/packages/spl-stake-pool/src/coder/events.ts index e7287f5b46..f84318df22 100644 --- a/ts/packages/spl-stake-pool/src/coder/events.ts +++ b/ts/packages/spl-stake-pool/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplStakePoolEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplStakePool program does not have events"); +export class SplStakePoolEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-stateless-asks/src/coder/events.ts b/ts/packages/spl-stateless-asks/src/coder/events.ts index 753f540a90..d8b9e3afa8 100644 --- a/ts/packages/spl-stateless-asks/src/coder/events.ts +++ b/ts/packages/spl-stateless-asks/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplStatelessAsksEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplStatelessAsks program does not have events"); +export class SplStatelessAsksEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-token-lending/src/coder/events.ts b/ts/packages/spl-token-lending/src/coder/events.ts index 5a7b45f484..15beeb6641 100644 --- a/ts/packages/spl-token-lending/src/coder/events.ts +++ b/ts/packages/spl-token-lending/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplTokenLendingEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplTokenLending program does not have events"); +export class SplTokenLendingEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-token-swap/src/coder/events.ts b/ts/packages/spl-token-swap/src/coder/events.ts index 8e08855129..94275def48 100644 --- a/ts/packages/spl-token-swap/src/coder/events.ts +++ b/ts/packages/spl-token-swap/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplTokenSwapEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplTokenSwap program does not have events"); +export class SplTokenSwapEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } } diff --git a/ts/packages/spl-token/src/coder/events.ts b/ts/packages/spl-token/src/coder/events.ts index b33d23bb7a..77a82f7892 100644 --- a/ts/packages/spl-token/src/coder/events.ts +++ b/ts/packages/spl-token/src/coder/events.ts @@ -1,12 +1,7 @@ -import { Idl, Event, EventCoder } from "@coral-xyz/anchor"; -import { IdlEvent } from "@coral-xyz/anchor/dist/cjs/idl"; +import { Idl, EventCoder, NoEventCoder } from "@coral-xyz/anchor"; -export class SplTokenEventsCoder implements EventCoder { - constructor(_idl: Idl) {} - - decode>( - _log: string - ): Event | null { - throw new Error("SplToken program does not have events"); +export class SplTokenEventsCoder extends NoEventCoder implements EventCoder { + constructor(_idl: Idl) { + super(); } }