@atomiqlabs/chain-starknet
Version:
Starknet specific base implementation
156 lines (142 loc) • 6.88 kB
text/typescript
import {Abi} from "abi-wan-kanabi";
import {EventToPrimitiveType, ExtractAbiEventNames} from "abi-wan-kanabi/dist/kanabi";
import {StarknetEvent, StarknetEvents} from "../../chain/modules/StarknetEvents";
import {CallData, events, hash, createAbiParser, AbiEvents, AbiStructs, AbiEnums} from "starknet";
import {StarknetContractBase} from "../StarknetContractBase";
import {toHex} from "../../../utils/Utils";
import {StarknetChainInterface} from "../../chain/StarknetChainInterface";
export type StarknetAbiEvent<TAbi extends Abi, TEventName extends ExtractAbiEventNames<TAbi>> = {
name: TEventName,
params: EventToPrimitiveType<TAbi, TEventName>,
txHash: string,
blockHash?: string,
blockNumber?: number,
keys: string[],
data: string[]
};
export class StarknetContractEvents<TAbi extends Abi> extends StarknetEvents {
private readonly contract: StarknetContractBase<TAbi>;
private readonly abi: TAbi;
private readonly knownEventNames: string[];
private readonly abiEvents: AbiEvents;
private readonly abiStructs: AbiStructs;
private readonly abiEnums: AbiEnums;
constructor(chainInterface: StarknetChainInterface, contract: StarknetContractBase<TAbi>, abi: TAbi) {
super(chainInterface);
this.contract = contract;
this.abi = abi;
this.abiEvents = events.getAbiEvents(this.abi);
this.abiStructs = CallData.getAbiStruct(this.abi);
this.abiEnums = CallData.getAbiEnum(this.abi);
this.knownEventNames = Object.keys(this.abiEvents).map(hash => this.abiEvents[hash].name as string);
}
public toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[] {
return blockEvents.map((starknetEvent) => {
// Convert StarknetEvent to EMITTED_EVENT format expected by parseEvents
const [value] = events.parseEvents(
[{
...starknetEvent,
transaction_index: starknetEvent.transaction_index ?? 0,
event_index: starknetEvent.event_index ?? 0
}],
this.abiEvents, this.abiStructs, this.abiEnums, createAbiParser(this.abi)
);
if(value==null) throw new Error("Invalid event detected, please check provided ABI");
const name = Object.keys(value).find(name => this.knownEventNames.includes(name));
if(name==null) throw new Error("Invalid event detected (name), please check provided ABI");
const event: StarknetAbiEvent<TAbi, T> = {
name: name as T,
txHash: starknetEvent.transaction_hash,
params: value[name] as any,
blockNumber: starknetEvent.block_number,
blockHash: starknetEvent.block_hash,
data: starknetEvent.data,
keys: starknetEvent.keys
}
// this.logger.debug("toStarknetAbiEvents(): Parsed event: ", event);
return event;
});
}
public toFilter<T extends ExtractAbiEventNames<TAbi>>(
events: T[],
keys: null | (null | string | string[])[],
): string[][] {
const filterArray: string[][] = [];
filterArray.push(events.map(name => {
const arr = name.split(":");
const eventName = arr[arr.length-1];
return toHex(hash.starknetKeccak(eventName), 0)
}));
if(keys!=null) keys.forEach(key => filterArray.push(key==null ? [] : Array.isArray(key) ? key.map(k => toHex(k, 0)) : [toHex(key, 0)]));
return filterArray;
}
/**
* Returns the events occuring in a range of starknet block as identified by the contract and keys,
* returns pending events if no startHeight & endHeight is passed
*
* @param events
* @param keys
* @param startBlockHeight
* @param endBlockHeight
*/
public async getContractBlockEvents<T extends ExtractAbiEventNames<TAbi>>(
events: T[],
keys: (null | string | string[])[],
startBlockHeight?: number,
endBlockHeight?: number | null
): Promise<StarknetAbiEvent<TAbi, T>[]> {
if(endBlockHeight===undefined) endBlockHeight = startBlockHeight;
const blockEvents = await super.getBlockEvents(this.contract.contract.address, this.toFilter(events, keys), startBlockHeight, endBlockHeight);
return this.toStarknetAbiEvents(blockEvents);
}
/**
* Runs a search backwards in time, processing the events for a specific topic public key
*
* @param events
* @param keys
* @param processor called for every event, should return a value if the correct event was found, or null
* if the search should continue
* @param abortSignal
*/
public async findInContractEvents<T, TEvent extends ExtractAbiEventNames<TAbi>>(
events: TEvent[],
keys: null | (null | string | string[])[],
processor: (event: StarknetAbiEvent<TAbi, TEvent>) => Promise<T | null>,
abortSignal?: AbortSignal
) {
return this.findInEvents<T>(this.contract.contract.address, this.toFilter(events, keys), async (events: StarknetEvent[]) => {
const parsedEvents = this.toStarknetAbiEvents<TEvent>(events);
for(let event of parsedEvents) {
const result = await processor(event);
if(result!=null) return result;
}
return null;
}, abortSignal);
}
/**
* Runs a search forwards in time, processing the events for a specific topic public key
*
* @param events
* @param keys
* @param processor called for every event, should return a value if the correct event was found, or null
* if the search should continue
* @param startHeight
* @param abortSignal
*/
public async findInContractEventsForward<T, TEvent extends ExtractAbiEventNames<TAbi>>(
events: TEvent[],
keys: null | (null | string | string[])[],
processor: (event: StarknetAbiEvent<TAbi, TEvent>) => Promise<T | null>,
startHeight?: number,
abortSignal?: AbortSignal
) {
return this.findInEventsForward<T>(this.contract.contract.address, this.toFilter(events, keys), async (events: StarknetEvent[]) => {
const parsedEvents = this.toStarknetAbiEvents<TEvent>(events);
for(let event of parsedEvents) {
const result = await processor(event);
if(result!=null) return result;
}
return null;
}, Math.max(startHeight ?? 0, this.contract._contractDeploymentHeight ?? 0), abortSignal);
}
}