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

feat: add support for parsing emitted events #1227

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions __tests__/cairo1v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,9 @@ describe('Cairo 1', () => {
maker_source: 418413900385n,
taker_source: 418413900385n,
},
block_hash: '0x39f27ab4cd508ab99e818512b261a7e4ae01072eb4ec8bb86aeb64755f99f2c',
block_number: 69198,
transaction_hash: '0x4e38fcce79c115b6fe2c486e3514efc1bd4da386b91c104e97230177d0bf181',
},
]);
// From component `DepositComponent`, event `Deposit` (same event name than next)
Expand Down Expand Up @@ -1093,6 +1096,9 @@ describe('Cairo 1', () => {
funder: 1466771120193999006693452314154095230636738457276435850562375218974960297344n,
amount: 4956000000000000n,
},
block_hash: '0x31afd649a5042cb1855ce820708a555eab62fe6ea07a2a538fa9100cdc80383',
block_number: 69198,
transaction_hash: '0x7768860d79bfb4c8463d215abea3c267899e373407c6882077f7447051c50de',
},
]);
const parsedEventNestedDeposit2 = events.parseEvents(
Expand All @@ -1109,6 +1115,9 @@ describe('Cairo 1', () => {
funder: 1466771120193999006693452314154095230636738457276435850562375218974960297344n,
amount: 4956000000000000n,
},
block_hash: '0x39f27ab4cd508ab99e818512b261a7e4ae01072eb4ec8bb86aeb64755f99f2c',
block_number: 69198,
transaction_hash: '0x2d5210e5334a83306abe6f7f5e7e65cd1feed72ad3b8e359a2f4614fa948e1d',
},
]);

Expand All @@ -1133,6 +1142,9 @@ describe('Cairo 1', () => {
to: 2087021424722619777119509474943472645767659996348769578120564519014510906823n,
value: 4956000000000000n,
},
block_hash: '0x39f27ab4cd508ab99e818512b261a7e4ae01072eb4ec8bb86aeb64755f99f2c',
block_number: 69198,
transaction_hash: '0x2da31a929a9848e9630906275a75a531e1718d4830501e10b0bccacd55f6fe0',
},
]);
});
Expand Down
85 changes: 85 additions & 0 deletions __tests__/utils/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,91 @@ describe('parseEvents', () => {
expect(parsedEvents).toStrictEqual(result);
});

test('should return parsed emitted events', () => {
const abiEventAndVariantName = 'cairo_event_struct';
const abiCairoEventStruct: AbiEvent = {
kind: 'struct',
members: [
{
name: 'test_name',
type: 'test_type',
kind: 'data',
},
],
name: abiEventAndVariantName,
type: 'event',
};

const abiCairoEventEnum: CairoEventVariant = {
kind: 'enum',
variants: [
{
name: 'test_name',
type: abiEventAndVariantName,
kind: 'data',
},
],
name: 'test_cairo_event',
type: 'event',
};

const abiEvents = getAbiEvents([getInterfaceAbi(), abiCairoEventStruct, abiCairoEventEnum]);

const abiStructs: AbiStructs = {
abi_structs: {
members: [
{
name: 'test_name',
type: 'test_type',
offset: 1,
},
],
size: 2,
name: 'cairo_event_struct',
type: 'struct',
},
};

const abiEnums: AbiEnums = {
abi_enums: {
variants: [
{
name: 'test_name',
type: 'cairo_event_struct_variant',
offset: 1,
},
],
size: 2,
name: 'test_cairo_event',
type: 'enum',
},
};

const event: RPC.EmittedEvent = {
from_address: 'test_address',
keys: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'],
data: ['0x3c719ce4f57dd2d9059b9ffed65417d694a29982d35b188574144d6ae6c3f87'],
block_hash: '0x26b160f10156dea0639bec90696772c640b9706a47f5b8c52ea1abe5858b34d',
block_number: 1,
transaction_hash: '0x26b160f10156dea0639bec90696772c640b9706a47f5b8c52ea1abe5858b34c',
};

const parsedEvents = parseEvents([event], abiEvents, abiStructs, abiEnums);

const result = [
{
cairo_event_struct: {
test_name: 1708719217404197029088109386680815809747762070431461851150711916567020191623n,
},
block_hash: '0x26b160f10156dea0639bec90696772c640b9706a47f5b8c52ea1abe5858b34d',
block_number: 1,
transaction_hash: '0x26b160f10156dea0639bec90696772c640b9706a47f5b8c52ea1abe5858b34c',
},
];

expect(parsedEvents).toStrictEqual(result);
});

test('should throw if ABI events has not enough data in "keys" property', () => {
const abiEventAndVariantName = 'cairo_event_struct';
const abiCairoEventStruct: AbiEvent = {
Expand Down
8 changes: 7 additions & 1 deletion src/types/contract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { BlockHash, TransactionHash } from 'starknet-types-07';
import { CairoEnum } from './cairoEnum';
import {
BigNumberish,
BlockIdentifier,
BlockNumber,
Calldata,
ParsedStruct,
RawArgsArray,
Expand Down Expand Up @@ -44,6 +46,10 @@ export type InvokeOptions = Pick<
'maxFee' | 'nonce' | 'signature' | 'parseRequest'
>;

export type ParsedEvent = { [name: string]: ParsedStruct };
export type ParsedEvent = { [name: string]: ParsedStruct } & {
block_hash?: BlockHash;
block_number?: BlockNumber;
transaction_hash?: TransactionHash;
};

export type ParsedEvents = Array<ParsedEvent>;
93 changes: 49 additions & 44 deletions src/utils/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,57 +194,62 @@ function mergeAbiEvents(target: any, source: any): Object {
* ```
*/
export function parseEvents(
providerReceivedEvents: RPC.Event[],
providerReceivedEvents: RPC.EmittedEvent[] | RPC.Event[],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why RPC.EmittedEvent[] | RPC.Event[]?
Isn't it only RPC.EmittedEvent[]?
In which case are block_hash, block_number & transaction_hash not included?

Copy link
Author

Choose a reason for hiding this comment

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

Block/tx info is not included in a case where you first fetch transaction receipt with starknet_getTransactionReceipt. In those kinds of reponses RPC.Event struct is returned (see: this). On the other hand when fetching data with starknet_fetchEvents returned struct is RPC.EmittedEvents.

So I thought it would be nice to leave that as is in case someone had code like this

events.parseEvents(receipts.map(t => t.events),...)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that the root cause of this wrong input type of events.parseEvents() is due to Contract.parseEvent(). This method is calling events.parseEvents() with RPC.event format ; here is the root cause of all this mess.
I think that you have to modify Contract.parseEvent() :

  • if the event is included in a block, this type has to be sent to events.parseEvents() :
type EMITTED_EVENT = EVENT & {
  block_hash: BLOCK_HASH;
  block_number: BLOCK_NUMBER;
  transaction_hash: TXN_HASH;
};
  • if the event is in a PENDING transaction, this type has to be sent to events.parseEvents() :
type EMITTED_EVENT = EVENT & {
  transaction_hash: TXN_HASH;
};

events.parseEvents() has to accept both types as input.

RPC.Event[] type was here only to handle Contract.parseEvent() ; it's useless for users. They uses EVENTS_CHUNKtype (result of RpcProvider.getEvents()), that includes EmittedEvent type. Never RPC.Events.

Copy link
Author

Choose a reason for hiding this comment

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

Not sure how pending transaction have anything to do with this. AFAIK events are created and emitted during transaction execution. So there is always tx hash, block hash and block number corresponding to the event. They are missing from events array of the recipiet because that is always available in parent (receipt) object.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If PENDING is not appropriate, then we have still a problem in Contract.parseEvents() :

(receipt as InvokeTransactionReceiptResponse).events?.filter(

InvokeTransactionReceiptResponse type is not the good one, because it's made of INVOKE_TXN_RECEIPT | PENDING_INVOKE_TXN_RECEIPT
And in this case, only EMITTED_EVENT should be transfered from Contract.parseEvent() to events.parseEvents() (not RPC.event).
Comment about uselessness of RPC.event as input of events.parseEvents() remains valid.

Copy link
Author

Choose a reason for hiding this comment

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

So if I understood correctly, you would like Contract.parseEvent() to change and look like this?

  public parseEvents(receipt: GetTransactionReceiptResponse): ParsedEvents {
    return parseRawEvents(
      (receipt as InvokeTransactionReceiptResponse).events?.
      map(event => { return {
        block_hash: (receipt as INVOKE_TXN_RECEIPT).block_hash,
        block_number: (receipt as INVOKE_TXN_RECEIPT).block_number,
        transaction_hash: (receipt as INVOKE_TXN_RECEIPT).transaction_hash,
        ...event
      }})
      .filter(
        (event) => cleanHex(event.from_address) === cleanHex(this.address),
        []
      ) || [],
      this.events,
      this.structs,
      CallData.getAbiEnum(this.abi)
    );
  }

Copy link
Collaborator

@PhilippeR26 PhilippeR26 Sep 16, 2024

Choose a reason for hiding this comment

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

Yes, this way.
I tested successfully this :
account/default.ts :

public parseEvents(receipt: GetTransactionReceiptResponse): ParsedEvents {
let parsed: ParsedEvents;
receipt.match({
  success: (txR: SuccessfulTransactionReceiptResponse) => {
    const emittedEvents =
      (txR as InvokeTransactionReceiptResponse).events
        ?.map((event) => {
          return {
            block_hash: (txR as INVOKE_TXN_RECEIPT).block_hash,
            block_number: (txR as INVOKE_TXN_RECEIPT).block_number,
            transaction_hash: (txR as INVOKE_TXN_RECEIPT).transaction_hash,
            ...event,
          };
        })
        .filter((event) => cleanHex(event.from_address) === cleanHex(this.address), []) || [];
    parsed = parseRawEvents(
      emittedEvents,
      this.events,
      this.structs,
      CallData.getAbiEnum(this.abi)
    );
  },
  _: () => {
    throw Error('This transaction was not successful.');
  },
});
return parsed!;
}

events/index.ts :

export function parseEvents(
  providerReceivedEvents: RPC.EmittedEvent[],
  abiEvents: AbiEvents,
  abiStructs: AbiStructs,
  abiEnums: AbiEnums
): ParsedEvents {
...

abiEvents: AbiEvents,
abiStructs: AbiStructs,
abiEnums: AbiEnums
): ParsedEvents {
const ret = providerReceivedEvents.flat().reduce((acc, recEvent: RPC.Event) => {
let abiEvent: AbiEvent | AbiEvents = abiEvents[recEvent.keys.shift() ?? 0];
if (!abiEvent) {
return acc;
}
while (!abiEvent.name) {
const hashName = recEvent.keys.shift();
assert(!!hashName, 'Not enough data in "keys" property of this event.');
abiEvent = (abiEvent as AbiEvents)[hashName];
}
// Create our final event object
const parsedEvent: ParsedEvent = {};
parsedEvent[abiEvent.name as string] = {};
// Remove the event's name hashed from the keys array
const keysIter = recEvent.keys[Symbol.iterator]();
const dataIter = recEvent.data[Symbol.iterator]();
const ret = providerReceivedEvents
.flat()
.reduce((acc, recEvent: RPC.EmittedEvent | RPC.Event) => {
let abiEvent: AbiEvent | AbiEvents = abiEvents[recEvent.keys.shift() ?? 0];
if (!abiEvent) {
return acc;
}
while (!abiEvent.name) {
const hashName = recEvent.keys.shift();
assert(!!hashName, 'Not enough data in "keys" property of this event.');
abiEvent = (abiEvent as AbiEvents)[hashName];
}
// Create our final event object
const parsedEvent: ParsedEvent = {};
parsedEvent[abiEvent.name as string] = {};
// Remove the event's name hashed from the keys array
const keysIter = recEvent.keys[Symbol.iterator]();
const dataIter = recEvent.data[Symbol.iterator]();

const abiEventKeys =
(abiEvent as CairoEventDefinition).members?.filter((it) => it.kind === 'key') ||
(abiEvent as LegacyEvent).keys;
const abiEventData =
(abiEvent as CairoEventDefinition).members?.filter((it) => it.kind === 'data') ||
(abiEvent as LegacyEvent).data;
const abiEventKeys =
(abiEvent as CairoEventDefinition).members?.filter((it) => it.kind === 'key') ||
(abiEvent as LegacyEvent).keys;
const abiEventData =
(abiEvent as CairoEventDefinition).members?.filter((it) => it.kind === 'data') ||
(abiEvent as LegacyEvent).data;

abiEventKeys.forEach((key) => {
parsedEvent[abiEvent.name as string][key.name] = responseParser(
keysIter,
key,
abiStructs,
abiEnums,
parsedEvent[abiEvent.name as string]
);
});
abiEventKeys.forEach((key) => {
parsedEvent[abiEvent.name as string][key.name] = responseParser(
keysIter,
key,
abiStructs,
abiEnums,
parsedEvent[abiEvent.name as string]
);
});

abiEventData.forEach((data) => {
parsedEvent[abiEvent.name as string][data.name] = responseParser(
dataIter,
data,
abiStructs,
abiEnums,
parsedEvent[abiEvent.name as string]
);
});
acc.push(parsedEvent);
return acc;
}, [] as ParsedEvents);
abiEventData.forEach((data) => {
parsedEvent[abiEvent.name as string][data.name] = responseParser(
dataIter,
data,
abiStructs,
abiEnums,
parsedEvent[abiEvent.name as string]
);
});
if ('block_hash' in recEvent) parsedEvent.block_hash = recEvent.block_hash;
if ('block_number' in recEvent) parsedEvent.block_number = recEvent.block_number;
if ('transaction_hash' in recEvent) parsedEvent.transaction_hash = recEvent.transaction_hash;
acc.push(parsedEvent);
return acc;
}, [] as ParsedEvents);
return ret;
}

Expand Down