@make-software/ces-js-parser
Version:
Parser for contract-level events encoded with Casper Event Standard (CES)
280 lines (229 loc) • 7.06 kB
text/typescript
import {
CLTypeByteArray,
CLTypeList,
CLTypeString,
CLValueParser,
CLValueUInt32,
Conversions,
ExecutionResult,
Hash,
IResultWithBytes,
RpcClient,
TypeID,
} from 'casper-js-sdk';
import {
Event,
parseEventDataFromBytes,
parseEventNameWithRemainder,
} from './event';
import { parseSchemasFromBytes, Schemas } from './schema';
export interface ContractMetadata {
schemas: Schemas;
contractHash: Hash;
contractPackageHash: Hash;
eventsSchemaUref: string;
eventsUref: string;
}
interface Dictionary {
uref: string;
key: string;
value: Uint8Array;
}
export const EVENTS_SCHEMA_NAMED_KEY = '__events_schema';
export const EVENTS_NAMED_KEY = '__events';
export interface ParseResult {
error: string | null;
event: Event;
}
export class Parser {
constructor(
private readonly contractsMetadata: Record<string, ContractMetadata>,
) {}
static async create(
rpcClient: RpcClient,
contractHashes: string[],
): Promise<Parser> {
const contractsMetadata = await Parser.getContractsMetadata(
rpcClient,
contractHashes,
);
return Promise.resolve(new Parser(contractsMetadata));
}
private static async getContractsMetadata(
rpcClient: RpcClient,
contractHashes: string[],
): Promise<Record<string, ContractMetadata>> {
const contractsSchemas: Record<string, ContractMetadata> = {};
const stateRootHash = await rpcClient.getStateRootHashLatest();
for (const contractHash of contractHashes) {
const contractData = (
await rpcClient.getStateItem(
stateRootHash.stateRootHash.toHex(),
`hash-${contractHash}`,
[],
)
).storedValue.contract;
const namedKeys = Object.values(contractData!.namedKeys);
if (!contractData) {
throw new Error('contract data not found');
}
let eventsSchemaUref = '';
let eventsUref = '';
for (const namedKey of namedKeys) {
if (namedKey.name === EVENTS_SCHEMA_NAMED_KEY) {
eventsSchemaUref = namedKey.key;
} else if (namedKey.name === EVENTS_NAMED_KEY) {
eventsUref = namedKey.key;
}
if (eventsSchemaUref !== '' && eventsUref !== '') {
break;
}
}
if (!eventsSchemaUref) {
throw new Error(`no '${EVENTS_SCHEMA_NAMED_KEY}' uref found`);
}
if (!eventsUref) {
throw new Error(`no '${EVENTS_NAMED_KEY}' uref found`);
}
const schemaResponse = await rpcClient.getStateItem(
stateRootHash.stateRootHash.toHex(),
eventsSchemaUref.toString(),
[],
);
if (!schemaResponse.storedValue.clValue) {
throw new Error(`no schema uref for ${eventsSchemaUref}`);
}
const schemas = parseSchemasFromBytes(
Conversions.decodeBase16(
schemaResponse.rawJSON.stored_value.CLValue.bytes,
),
);
contractsSchemas[eventsUref.toString()] = {
schemas,
contractHash: Hash.fromHex(contractHash),
contractPackageHash: contractData.contractPackageHash.hash,
eventsSchemaUref,
eventsUref,
};
}
return contractsSchemas;
}
public parseExecutionResult(executionResult: ExecutionResult): ParseResult[] {
if (!executionResult.effects || executionResult.effects.length === 0) {
throw new Error(`failed transaction ${executionResult.errorMessage}`);
}
const results: ParseResult[] = [];
for (const transform of executionResult.effects) {
if (!transform.key.dictionary) {
continue;
}
if (!transform.kind.isWriteCLValue()) {
continue;
}
const clValue = transform.kind.parseAsWriteCLValue();
if (!clValue.any) {
continue;
}
let dictionary: Dictionary;
let eventNameWithRemainder: IResultWithBytes<string>;
try {
dictionary = this.newDictionaryFromBytes(clValue.any.bytes());
eventNameWithRemainder = parseEventNameWithRemainder(dictionary.value);
} catch (err) {
continue;
}
const parsedEvent: Event = {
contractHash: null,
contractPackageHash: null,
name: eventNameWithRemainder.result,
eventId: parseInt(dictionary.key),
data: {},
};
const contractMetadata = this.contractsMetadata[dictionary.uref];
if (!contractMetadata) {
results.push({
event: parsedEvent,
error: `invalid event uref`,
});
continue;
}
parsedEvent.contractHash = contractMetadata.contractHash;
parsedEvent.contractPackageHash = contractMetadata.contractPackageHash;
const eventSchema =
contractMetadata.schemas[eventNameWithRemainder.result];
if (!eventSchema) {
results.push({
event: parsedEvent,
error: `event name not in schema`,
});
continue;
}
try {
parsedEvent.data = parseEventDataFromBytes(
eventSchema,
eventNameWithRemainder.bytes,
);
} catch (err) {
results.push({
event: parsedEvent,
error: `failed to parse event data bytes, err: ${err.message}`,
});
continue;
}
results.push({
event: parsedEvent,
error: null,
});
}
return results;
}
newDictionaryFromBytes(data: Uint8Array): Dictionary {
const clValue = CLValueParser.fromBytesWithType(data);
if (
!(clValue.result.type instanceof CLTypeList) ||
clValue.result.type.elementsType.getTypeID() !== TypeID.U8
) {
throw new Error('failed to parse CLList(CLU8) from bytes');
}
const clValueByteSize = CLValueUInt32.fromBytes(clValue.bytes);
const clByteArrayAsUref = CLValueParser.fromBytesByType(
clValueByteSize.bytes,
new CLTypeByteArray(clValueByteSize.result.toNumber()),
);
if (!clByteArrayAsUref.result.byteArray) {
throw new Error('failed to parse CLByteArray from bytes');
}
const uref = `uref-${Conversions.encodeBase16(
clByteArrayAsUref.result.byteArray.bytes(),
)}-007`;
const clStringAsDictKey = CLValueParser.fromBytesByType(
clByteArrayAsUref.bytes,
CLTypeString,
);
if (!clStringAsDictKey.result.stringVal) {
throw new Error('failed to parse CLString from bytes');
}
return {
uref: uref,
key: clStringAsDictKey.result.stringVal.toString(),
value: new Uint8Array(
clValue.result.list!.elements.map(el => el.ui8!.toNumber()),
),
};
}
}
export async function fetchContractSchemasBytes(
rpcClient: RpcClient,
contractHash: string,
stateRootHash: string,
): Promise<Uint8Array> {
const schemaResponse = await rpcClient.getStateItem(
stateRootHash,
`hash-${contractHash}`,
[EVENTS_SCHEMA_NAMED_KEY],
);
if (!schemaResponse.storedValue.clValue) {
throw new Error('no clvalue for contract schema');
}
return schemaResponse.storedValue.clValue.bytes();
}