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

Handle "events" in abi registry #343

Merged
merged 10 commits into from
Nov 13, 2023
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ MultiversX SDK for JavaScript and TypeScript (written in TypeScript).

## Documentation

- [Cookbook](https://docs.multiversx.com/sdk-and-tools/erdjs/erdjs-cookbook/)
- [Cookbook](https://docs.multiversx.com/sdk-and-tools/sdk-js/sdk-js-cookbook/)

## Distribution

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@multiversx/sdk-core",
"version": "12.13.0",
"version": "12.14.0",
"description": "MultiversX SDK for JavaScript and TypeScript",
"main": "out/index.js",
"types": "out/index.d.js",
Expand Down
8 changes: 8 additions & 0 deletions src/interfaceOfNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ export interface ITransactionLogs {
events: ITransactionEvent[];

findSingleOrNoneEvent(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent | undefined;

/**
* @deprecated Will be removed from the interface (with no replacement). Not used in "sdk-core".
*/
findFirstOrNoneEvent(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent | undefined;

/**
* @deprecated Will be removed from the interface (with no replacement). Not used in "sdk-core".
*/
findEvents(identifier: string, predicate?: (event: ITransactionEvent) => boolean): ITransactionEvent[];
}

Expand Down
14 changes: 9 additions & 5 deletions src/smartcontracts/argSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ARGUMENTS_SEPARATOR } from "../constants";
import { BinaryCodec } from "./codec";
import { EndpointParameterDefinition, Type, TypedValue, U32Type, U32Value } from "./typesystem";
import { Type, TypedValue, U32Type, U32Value } from "./typesystem";
import { OptionalType, OptionalValue } from "./typesystem/algebraic";
import { CompositeType, CompositeValue } from "./typesystem/composite";
import { VariadicType, VariadicValue } from "./typesystem/variadic";
Expand All @@ -15,24 +15,28 @@ interface ICodec {
encodeTopLevel(typedValue: TypedValue): Buffer;
}

interface IParameterDefinition {
type: Type;
}

// TODO: perhaps move default construction options to a factory (ArgSerializerFactory), instead of referencing them in the constructor
// (postpone as much as possible, breaking change)
const defaultArgSerializerrOptions: IArgSerializerOptions = {
const defaultArgSerializerOptions: IArgSerializerOptions = {
codec: new BinaryCodec()
};

export class ArgSerializer {
codec: ICodec;

constructor(options?: IArgSerializerOptions) {
options = { ...defaultArgSerializerrOptions, ...options };
options = { ...defaultArgSerializerOptions, ...options };
this.codec = options.codec;
}

/**
* Reads typed values from an arguments string (e.g. aa@bb@@cc), given parameter definitions.
*/
stringToValues(joinedString: string, parameters: EndpointParameterDefinition[]): TypedValue[] {
stringToValues(joinedString: string, parameters: IParameterDefinition[]): TypedValue[] {
let buffers = this.stringToBuffers(joinedString);
let values = this.buffersToValues(buffers, parameters);
return values;
Expand All @@ -49,7 +53,7 @@ export class ArgSerializer {
/**
* Decodes a set of buffers into a set of typed values, given parameter definitions.
*/
buffersToValues(buffers: Buffer[], parameters: EndpointParameterDefinition[]): TypedValue[] {
buffersToValues(buffers: Buffer[], parameters: IParameterDefinition[]): TypedValue[] {
// TODO: Refactor, split (function is quite complex).
const self = this;

Expand Down
125 changes: 124 additions & 1 deletion src/smartcontracts/resultsParser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { ContractQueryResponse, ContractResultItem, ContractResults, TransactionEvent, TransactionEventTopic, TransactionLogs, TransactionOnNetwork } from "@multiversx/sdk-network-providers";
import { TransactionEventData } from "@multiversx/sdk-network-providers/out/transactionEvents";
import BigNumber from "bignumber.js";
import { assert } from "chai";
import * as fs from "fs";
import path from "path";
import { Address } from "../address";
import { IAddress } from "../interface";
import { ITransactionOnNetwork } from "../interfaceOfNetwork";
import { LogLevel, Logger } from "../logger";
import { loadAbiRegistry } from "../testutils";
import { ArgSerializer } from "./argSerializer";
import { ResultsParser } from "./resultsParser";
import { ReturnCode } from "./returnCode";
import { BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, StringType, StringValue, TypedValue, U32Type, U32Value, U64Type, U64Value, VariadicType, VariadicValue } from "./typesystem";
import { AbiRegistry, BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition, StringType, StringValue, TypedValue, U32Type, U32Value, U64Type, U64Value, VariadicType, VariadicValue } from "./typesystem";
import { BytesType, BytesValue } from "./typesystem/bytes";

const KnownReturnCodes: string[] = [
Expand Down Expand Up @@ -235,6 +239,125 @@ describe("test smart contract results parser", () => {
assert.deepEqual(bundle.values, []);
});

it("should parse contract event", async () => {
const abiRegistry = await loadAbiRegistry("src/testdata/esdt-safe.abi.json");
const eventDefinition = abiRegistry.getEvent("deposit");

const event = new TransactionEvent({
topics: [
new TransactionEventTopic("ZGVwb3NpdA=="),
new TransactionEventTopic("cmzC1LRt1r10pMhNAnFb+FyudjGMq4G8CefCYdQUmmc="),
new TransactionEventTopic("AAAADFdFR0xELTAxZTQ5ZAAAAAAAAAAAAAAAAWQ="),
],
dataPayload: new TransactionEventData(Buffer.from("AAAAAAAAA9sAAAA=", "base64"))
});

const bundle = parser.parseEvent(event, eventDefinition);

assert.equal((<IAddress>bundle.dest_address).bech32(), "erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun");
assert.equal(bundle.tokens[0].token_identifier, "WEGLD-01e49d");
assert.deepEqual(bundle.tokens[0].token_nonce, new BigNumber(0));
assert.deepEqual(bundle.tokens[0].amount, new BigNumber(100));
assert.deepEqual(bundle.event_data.tx_nonce, new BigNumber(987));
assert.isNull(bundle.event_data.opt_function);
assert.isNull(bundle.event_data.opt_arguments);
assert.isNull(bundle.event_data.opt_gas_limit);
});

it("should parse contract event (with multi-values)", async () => {
const abiRegistry = AbiRegistry.create({
"events": [
{
"identifier": "foobar",
"inputs": [
{
"name": "a",
"type": "multi<u8, utf-8 string, u8, utf-8 string>",
"indexed": true
},
{
"name": "b",
"type": "multi<utf-8 string, u8>",
"indexed": true
},
{
"name": "c",
"type": "u8",
"indexed": false
}
]
}
]
});

const eventDefinition = abiRegistry.getEvent("foobar");

const event = {
topics: [
new TransactionEventTopic(Buffer.from("not used").toString("base64")),
new TransactionEventTopic(Buffer.from([42]).toString("base64")),
new TransactionEventTopic(Buffer.from("test").toString("base64")),
new TransactionEventTopic(Buffer.from([43]).toString("base64")),
new TransactionEventTopic(Buffer.from("test").toString("base64")),
new TransactionEventTopic(Buffer.from("test").toString("base64")),
new TransactionEventTopic(Buffer.from([44]).toString("base64")),
],
dataPayload: new TransactionEventData(Buffer.from([42]))
};

const bundle = parser.parseEvent(event, eventDefinition);
assert.deepEqual(bundle.a, [new BigNumber(42), "test", new BigNumber(43), "test"]);
assert.deepEqual(bundle.b, ["test", new BigNumber(44)]);
assert.deepEqual(bundle.c, new BigNumber(42));
});

it("should parse contract event (Sirius)", async () => {
const abiRegistry = AbiRegistry.create({
"events": [
{
"identifier": "foobar",
"inputs": [
{
"name": "a",
"type": "u8",
"indexed": true
},
{
"name": "b",
"type": "u8",
"indexed": false
},
{
"name": "c",
"type": "u8",
"indexed": false
}
]
}
]
});

const eventDefinition = abiRegistry.getEvent("foobar");

const event = {
topics: [
new TransactionEventTopic(Buffer.from("not used").toString("base64")),
new TransactionEventTopic(Buffer.from([42]).toString("base64")),
],
additionalData: [
new TransactionEventData(Buffer.from([43])),
new TransactionEventData(Buffer.from([44]))
],
// Will be ignored.
dataPayload: new TransactionEventData(Buffer.from([43])),
};

const bundle = parser.parseEvent(event, eventDefinition);
assert.deepEqual(bundle.a, new BigNumber(42));
assert.deepEqual(bundle.b, new BigNumber(43));
assert.deepEqual(bundle.c, new BigNumber(44));
});

// This test should be enabled manually and run against a set of sample transactions.
// 2022-04-03: test ran against ~1800 transactions sampled from devnet.
it.skip("should parse real-world contract outcomes", async () => {
Expand Down
60 changes: 56 additions & 4 deletions src/smartcontracts/resultsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Logger } from "../logger";
import { ArgSerializer } from "./argSerializer";
import { TypedOutcomeBundle, UntypedOutcomeBundle } from "./interface";
import { ReturnCode } from "./returnCode";
import { EndpointDefinition, EndpointParameterDefinition, TypedValue } from "./typesystem";
import { Type, TypedValue } from "./typesystem";

enum WellKnownEvents {
OnTransactionCompleted = "completedTxEvent",
Expand All @@ -23,8 +23,24 @@ interface IResultsParserOptions {
argsSerializer: IArgsSerializer;
}

interface IParameterDefinition {
type: Type;
}

interface IEventInputDefinition {
name: string;
type: Type;
indexed: boolean;
}

interface ITransactionEvent {
readonly topics: { valueOf(): Uint8Array }[];
readonly dataPayload?: { valueOf(): Uint8Array };
readonly additionalData?: { valueOf(): Uint8Array }[];
}

interface IArgsSerializer {
buffersToValues(buffers: Buffer[], parameters: EndpointParameterDefinition[]): TypedValue[];
buffersToValues(buffers: Buffer[], parameters: IParameterDefinition[]): TypedValue[];
stringToBuffers(joinedString: string): Buffer[];
}

Expand All @@ -46,7 +62,7 @@ export class ResultsParser {
this.argsSerializer = options.argsSerializer;
}

parseQueryResponse(queryResponse: IContractQueryResponse, endpoint: EndpointDefinition): TypedOutcomeBundle {
parseQueryResponse(queryResponse: IContractQueryResponse, endpoint: { output: IParameterDefinition[] }): TypedOutcomeBundle {
let parts = queryResponse.getReturnDataParts();
let values = this.argsSerializer.buffersToValues(parts, endpoint.output);
let returnCode = new ReturnCode(queryResponse.returnCode.toString());
Expand All @@ -72,7 +88,7 @@ export class ResultsParser {
};
}

parseOutcome(transaction: ITransactionOnNetwork, endpoint: EndpointDefinition): TypedOutcomeBundle {
parseOutcome(transaction: ITransactionOnNetwork, endpoint: { output: IParameterDefinition[] }): TypedOutcomeBundle {
let untypedBundle = this.parseUntypedOutcome(transaction);
let values = this.argsSerializer.buffersToValues(untypedBundle.values, endpoint.output);

Expand Down Expand Up @@ -315,4 +331,40 @@ export class ResultsParser {
let returnCode = ReturnCode.fromBuffer(returnCodePart);
return { returnCode, returnDataParts };
}

parseEvent(transactionEvent: ITransactionEvent, eventDefinition: { inputs: IEventInputDefinition[] }): any {
const result: any = {};

// We skip the first topic, because that's the event identifier.
const topics = transactionEvent.topics.map(topic => Buffer.from(topic.valueOf())).slice(1);
// < Sirius.
const legacyData = transactionEvent.dataPayload?.valueOf() || Buffer.from([]);
// >= Sirius.
const additionalData = transactionEvent.additionalData?.map(data => Buffer.from(data.valueOf())) || [];

// < Sirius.
if (additionalData.length == 0) {
if (legacyData.length > 0) {
additionalData.push(Buffer.from(legacyData));
}
}

// "Indexed" ABI "event.inputs" correspond to "event.topics[1:]":
const indexedInputs = eventDefinition.inputs.filter(input => input.indexed);
const decodedTopics = this.argsSerializer.buffersToValues(topics, indexedInputs);

for (let i = 0; i < indexedInputs.length; i++) {
result[indexedInputs[i].name] = decodedTopics[i].valueOf();
}

// "Non-indexed" ABI "event.inputs" correspond to "event.data":
const nonIndexedInputs = eventDefinition.inputs.filter(input => !input.indexed);
const decodedDataParts = this.argsSerializer.buffersToValues(additionalData, nonIndexedInputs);

for (let i = 0; i < nonIndexedInputs.length; i++) {
result[nonIndexedInputs[i].name] = decodedDataParts[i].valueOf();
}

return result;
}
}
16 changes: 16 additions & 0 deletions src/smartcontracts/typesystem/abiRegistry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,20 @@ describe("test abi registry", () => {
assert.deepEqual(registry.getEndpoint("bar").output[0].type, new VariadicType(new U32Type(), true));
assert.deepEqual(registry.getEndpoint("bar").output[1].type, new VariadicType(new BytesType(), true));
});

it("should load ABI wih events", async () => {
const registry = await loadAbiRegistry("src/testdata/esdt-safe.abi.json");

assert.lengthOf(registry.events, 8);

const depositEvent = registry.getEvent("deposit");
assert.deepEqual(depositEvent.inputs[0].type, new AddressType());
assert.deepEqual(depositEvent.inputs[1].type, new ListType(registry.getCustomType("EsdtTokenPayment")));
assert.deepEqual(depositEvent.inputs[2].type, registry.getCustomType("DepositEvent"));

const setStatusEvent = registry.getEvent("setStatusEvent");
assert.deepEqual(setStatusEvent.inputs[0].type, new U64Type());
assert.deepEqual(setStatusEvent.inputs[1].type, new U64Type());
assert.deepEqual(setStatusEvent.inputs[2].type, registry.getCustomType("TransactionStatus"));
});
});
Loading
Loading