Skip to content

Commit

Permalink
Merge pull request #351 from multiversx/main-into-next-15-nov
Browse files Browse the repository at this point in the history
Main into next (2023-11-15)
  • Loading branch information
andreibancioiu committed Nov 15, 2023
2 parents ae859ab + 90b8c94 commit 91d1d43
Show file tree
Hide file tree
Showing 14 changed files with 1,042 additions and 34 deletions.
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": "13.0.0-alpha.0",
"version": "13.0.0-alpha.1",
"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
9 changes: 3 additions & 6 deletions src/signableMessage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Address } from "./address";
import { ISignature } from "./interface";
import { interpretSignatureAsBuffer } from "./signature";
const createKeccakHash = require("keccak");

export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n";
Expand Down Expand Up @@ -56,12 +57,8 @@ export class SignableMessage {
return this.signature;
}

applySignature(signature: ISignature | Buffer) {
if (signature instanceof Buffer) {
this.signature = signature;
} else {
this.signature = Buffer.from(signature.hex(), "hex");
}
applySignature(signature: ISignature | Uint8Array) {
this.signature = interpretSignatureAsBuffer(signature);
}

getMessageSize(): Buffer {
Expand Down
12 changes: 11 additions & 1 deletion src/signature.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as errors from "./errors";
import * as errors from "./errors";


const SIGNATURE_LENGTH = 64;
Expand Down Expand Up @@ -58,3 +58,13 @@ export class Signature {
return this.valueHex;
}
}

export function interpretSignatureAsBuffer(signature: { hex(): string; } | Uint8Array): Buffer {
if (ArrayBuffer.isView(signature)) {
return Buffer.from(signature);
} else if ((<any>signature).hex != null) {
return Buffer.from(signature.hex(), "hex");
}

throw new Error(`Object cannot be interpreted as a signature: ${signature}`);
}
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

0 comments on commit 91d1d43

Please sign in to comment.