Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ts: CPI events parsing #2886

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion tests/events/tests/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
86 changes: 85 additions & 1 deletion ts/packages/anchor/src/coder/borsh/event.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
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,
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.
*/
Expand Down Expand Up @@ -42,22 +57,36 @@ 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 {
logArr = base64.decode(log);
} 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;
Expand All @@ -70,4 +99,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could also support TransactionResponse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added the TransactionResponse to types.

When autogenerated, how can I run it that I can remove the extended class and make it as generated? I run the ./setup-tests.sh but I haven't found a guideline on development that can help me.

): { 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;
}
}
19 changes: 19 additions & 0 deletions ts/packages/anchor/src/coder/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VersionedTransactionResponse } from "@solana/web3.js";
import { IdlEvent } from "../idl.js";
import { Event } from "../program/event.js";

Expand Down Expand Up @@ -50,6 +51,24 @@ export interface EventCoder {
decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
log: string
): Event<E, T> | null;

parseCpiEvents<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
transactionResponse: VersionedTransactionResponse
): Event<E, T>[];
}

export abstract class NoEventCoder implements EventCoder {
decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error(`${this.constructor} program does not have events`);
}

parseCpiEvents<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_transactionResponse: VersionedTransactionResponse
): Event<E, T>[] {
throw new Error(`${this.constructor} program does not have CPI events`);
}
}

export interface TypesCoder<N extends string = string> {
Expand Down
8 changes: 7 additions & 1 deletion ts/packages/anchor/src/coder/system/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ 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) {}

decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("System program does not have events");
throw new Error("SystemProgram does not have events.");
}
parseCpiEvents<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_transactionResponse: VersionedTransactionResponse
): Event<E, T>[] {
throw new Error("SystemProgram does not have CPI events.");
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-associated-token-account/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplAssociatedTokenAccount program does not have events");
export class SplAssociatedTokenAccountEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-binary-option/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplBinaryOption program does not have events");
export class SplBinaryOptionEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-binary-oracle-pair/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplBinaryOraclePair program does not have events");
export class SplBinaryOraclePairEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-feature-proposal/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplFeatureProposal program does not have events");
export class SplFeatureProposalEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-governance/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplGovernance program does not have events");
export class SplGovernanceEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-memo/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplMemo program does not have events");
export class SplMemoEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-name-service/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplNameService program does not have events");
export class SplNameServiceEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-record/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplRecord program does not have events");
export class SplRecordEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-stake-pool/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplStakePool program does not have events");
export class SplStakePoolEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-stateless-asks/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplStatelessAsks program does not have events");
export class SplStatelessAsksEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
13 changes: 4 additions & 9 deletions ts/packages/spl-token-lending/src/coder/events.ts
Original file line number Diff line number Diff line change
@@ -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<E extends IdlEvent = IdlEvent, T = Record<string, string>>(
_log: string
): Event<E, T> | null {
throw new Error("SplTokenLending program does not have events");
export class SplTokenLendingEventsCoder extends NoEventCoder implements EventCoder {
constructor(_idl: Idl) {
super();
}
}
Loading
Loading