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

Implement: Smart Contract Transactions Outcome Parser #408

Merged
merged 27 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f9a978b
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 14, 2024
7e4f7f5
Sketch SC outcome parser.
andreibancioiu Mar 14, 2024
8f032d0
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 20, 2024
42337c9
TransactionOutcome: renaming, plus new field for direct contract outc…
andreibancioiu Mar 20, 2024
e834948
Renaming.
andreibancioiu Mar 20, 2024
74a0cc1
Outcome parser - work in progress.
andreibancioiu Mar 20, 2024
c4a265c
Refactoring, adjust result.
andreibancioiu Mar 20, 2024
ae3e389
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 21, 2024
c72db77
Reference newer network providers & wallet (dev-dependencies, for tes…
andreibancioiu Mar 21, 2024
137503e
Allow one to pass a legacy, but custom ResultsParser to the new SC tr…
andreibancioiu Mar 21, 2024
d682f56
Add some tests.
andreibancioiu Mar 21, 2024
403bcdb
Extra tests.
andreibancioiu Mar 21, 2024
dead1be
Handle missing function.
andreibancioiu Mar 21, 2024
5d366fe
Test converter, fix converter.
andreibancioiu Mar 22, 2024
c04b3ea
Return typed values from SC txs outcome parser, as well.
andreibancioiu Mar 22, 2024
6d250c2
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 22, 2024
f985169
Sketch parse events.
andreibancioiu Mar 22, 2024
fffd65d
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 22, 2024
651bd20
Bit of cleanup.
andreibancioiu Mar 22, 2024
678c46e
AdditionalData on events.
andreibancioiu Mar 22, 2024
80381c2
Fix after self review.
andreibancioiu Mar 22, 2024
0085207
findEventsByPredicate.
andreibancioiu Mar 22, 2024
67ea865
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 22, 2024
7fe17c0
Undo change.
andreibancioiu Mar 22, 2024
09e396e
Undo change.
andreibancioiu Mar 22, 2024
348c13f
Fix after review / fix tests.
andreibancioiu Mar 22, 2024
1735e12
Merge branch 'feat/next' into sc-outcome-parser
andreibancioiu Mar 22, 2024
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
28 changes: 14 additions & 14 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"keccak": "3.0.2"
},
"devDependencies": {
"@multiversx/sdk-network-providers": "2.3.0",
"@multiversx/sdk-wallet": "4.2.0",
"@multiversx/sdk-network-providers": "2.4.1",
"@multiversx/sdk-wallet": "4.4.0",
"@types/assert": "1.4.6",
"@types/chai": "4.2.11",
"@types/mocha": "9.1.0",
Expand Down
61 changes: 61 additions & 0 deletions src/converters/transactionsConverter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { IPlainTransactionObject, ITransaction } from "../interface";
import { IContractResultItem, ITransactionEvent, ITransactionOnNetwork } from "../interfaceOfNetwork";
import { ResultsParser } from "../smartcontracts";
import { Transaction } from "../transaction";
import {
SmartContractCallOutcome,
SmartContractResult,
TransactionEvent,
TransactionLogs,
TransactionOutcome,
} from "../transactionsOutcomeParsers/resources";

export class TransactionsConverter {
public transactionToPlainObject(transaction: ITransaction): IPlainTransactionObject {
Expand Down Expand Up @@ -61,4 +70,56 @@ export class TransactionsConverter {
private bufferFromHex(value?: string) {
return Buffer.from(value || "", "hex");
}

public transactionOnNetworkToOutcome(transactionOnNetwork: ITransactionOnNetwork): TransactionOutcome {
// In the future, this will not be needed because the transaction, as returned from the API,
// will hold the data corresponding to the direct smart contract call outcome (in case of smart contract calls).
const legacyResultsParser = new ResultsParser();
const callOutcomeBundle = legacyResultsParser.parseUntypedOutcome(transactionOnNetwork);
const callOutcome = new SmartContractCallOutcome({
function: transactionOnNetwork.function,
returnCode: callOutcomeBundle.returnCode.toString(),
returnMessage: callOutcomeBundle.returnMessage,
returnDataParts: callOutcomeBundle.values,
});

const contractResults = transactionOnNetwork.contractResults.items.map((result) =>
this.smartContractResultOnNetworkToSmartContractResult(result),
);

const logs = new TransactionLogs({
address: transactionOnNetwork.logs.address.bech32(),
events: transactionOnNetwork.logs.events.map((event) => this.eventOnNetworkToEvent(event)),
});

return new TransactionOutcome({
logs: logs,
smartContractResults: contractResults,
directSmartContractCallOutcome: callOutcome,
});
}

private smartContractResultOnNetworkToSmartContractResult(
resultOnNetwork: IContractResultItem,
): SmartContractResult {
return new SmartContractResult({
sender: resultOnNetwork.sender.bech32(),
receiver: resultOnNetwork.receiver.bech32(),
data: Buffer.from(resultOnNetwork.data),
logs: new TransactionLogs({
address: resultOnNetwork.logs.address.bech32(),
events: resultOnNetwork.logs.events.map((event) => this.eventOnNetworkToEvent(event)),
}),
});
}

private eventOnNetworkToEvent(eventOnNetwork: ITransactionEvent): TransactionEvent {
return new TransactionEvent({
address: eventOnNetwork.address.bech32(),
identifier: eventOnNetwork.identifier,
topics: eventOnNetwork.topics.map((topic) => Buffer.from(topic.hex(), "hex")),
data: eventOnNetwork.dataPayload?.valueOf() || Buffer.from(eventOnNetwork.data),
additionalData: eventOnNetwork.additionalData?.map((data) => data.valueOf()) || [],
});
}
}
203 changes: 201 additions & 2 deletions src/converters/transactionsConverters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import {
ContractResultItem,
ContractResults,
TransactionEventData,
TransactionEvent as TransactionEventOnNetwork,
TransactionEventTopic,
TransactionLogs as TransactionLogsOnNetwork,
TransactionOnNetwork,
} from "@multiversx/sdk-network-providers";
import { assert } from "chai";
import { Address } from "../address";
import { Transaction } from "../transaction";
import {
SmartContractCallOutcome,
SmartContractResult,
TransactionEvent,
TransactionLogs,
TransactionOutcome,
} from "../transactionsOutcomeParsers/resources";
import { TransactionsConverter } from "./transactionsConverter";

describe("test transactions converter", async () => {
it("converts transaction to plain object and back", () => {
const converter = new TransactionsConverter();

const transaction = new Transaction({
nonce: 90,
value: BigInt("123456789000000000000000000000"),
Expand All @@ -14,10 +34,189 @@ describe("test transactions converter", async () => {
gasLimit: 80000,
data: Buffer.from("hello"),
chainID: "localnet",
version: 2,
});

const plainObject = transaction.toPlainObject();
const restoredTransaction = Transaction.fromPlainObject(plainObject);
const plainObject = converter.transactionToPlainObject(transaction);
const restoredTransaction = converter.plainObjectToTransaction(plainObject);

assert.deepEqual(plainObject, transaction.toPlainObject());
assert.deepEqual(restoredTransaction, Transaction.fromPlainObject(plainObject));
assert.deepEqual(restoredTransaction, transaction);
assert.deepEqual(plainObject, {
nonce: 90,
value: "123456789000000000000000000000",
sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
receiver: "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
senderUsername: "YWxpY2U=",
receiverUsername: "Ym9i",
gasPrice: 1000000000,
gasLimit: 80000,
data: "aGVsbG8=",
chainID: "localnet",
version: 2,
options: undefined,
guardian: undefined,
signature: undefined,
guardianSignature: undefined,
});
});

it("converts transaction on network to transaction outcome", () => {
const converter = new TransactionsConverter();

const transactionOnNetwork = new TransactionOnNetwork({
nonce: 7,
function: "hello",
logs: new TransactionLogsOnNetwork({
address: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"),
events: [
new TransactionEventOnNetwork({
identifier: "foobar",
topics: [],
dataPayload: new TransactionEventData(Buffer.from("foo")),
additionalData: [],
}),
],
}),
contractResults: new ContractResults([
new ContractResultItem({
nonce: 8,
data: "@6f6b@2a",
logs: new TransactionLogsOnNetwork({
address: Address.fromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"),
events: [
new TransactionEventOnNetwork({
identifier: "writeLog",
topics: [
new TransactionEventTopic(
// '@too much gas provided for processing: gas provided = 596384500, gas used = 733010'
"QHRvbyBtdWNoIGdhcyBwcm92aWRlZCBmb3IgcHJvY2Vzc2luZzogZ2FzIHByb3ZpZGVkID0gNTk2Mzg0NTAwLCBnYXMgdXNlZCA9IDczMzAxMA==",
),
],
dataPayload: TransactionEventData.fromBase64("QDZmNmI="),
}),
],
}),
}),
]),
});

const actualTransactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork);
const expectedTransactionOutcome = new TransactionOutcome({
directSmartContractCallOutcome: new SmartContractCallOutcome({
function: "hello",
returnCode: "ok",
returnMessage: "ok",
returnDataParts: [Buffer.from([42])],
}),
smartContractResults: [
new SmartContractResult({
sender: "",
receiver: "",
data: Buffer.from("@6f6b@2a"),
logs: {
address: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8",
events: [
new TransactionEvent({
address: "",
identifier: "writeLog",
topics: [
Buffer.from(
"@too much gas provided for processing: gas provided = 596384500, gas used = 733010",
),
],
data: Buffer.from("QDZmNmI=", "base64"),
}),
],
},
}),
],
logs: new TransactionLogs({
address: "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8",
events: [
new TransactionEvent({
address: "",
identifier: "foobar",
topics: [],
data: Buffer.from("foo"),
additionalData: [],
}),
],
}),
});

assert.deepEqual(actualTransactionOutcome, expectedTransactionOutcome);
});

it("converts transaction on network to transaction outcome (with signal error)", () => {
const converter = new TransactionsConverter();

const transactionOnNetwork = new TransactionOnNetwork({
nonce: 42,
function: "hello",
contractResults: new ContractResults([
new ContractResultItem({
nonce: 42,
data: "@657865637574696f6e206661696c6564",
logs: new TransactionLogsOnNetwork({
address: Address.fromBech32("erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x"),
events: [
new TransactionEventOnNetwork({
address: Address.fromBech32(
"erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x",
),
identifier: "signalError",
topics: [
new TransactionEventTopic("XmC5/yOF6ie6DD2kaJd5qPc2Ss7h2w7nvuWaxmCiiXQ="),
new TransactionEventTopic("aW5zdWZmaWNpZW50IGZ1bmRz"),
],
dataPayload: new TransactionEventData(Buffer.from("@657865637574696f6e206661696c6564")),
additionalData: [
new TransactionEventData(Buffer.from("foo")),
new TransactionEventData(Buffer.from("bar")),
],
}),
],
}),
}),
]),
});

const actualTransactionOutcome = converter.transactionOnNetworkToOutcome(transactionOnNetwork);
const expectedTransactionOutcome = new TransactionOutcome({
directSmartContractCallOutcome: new SmartContractCallOutcome({
function: "hello",
returnCode: "execution failed",
returnMessage: "execution failed",
returnDataParts: [],
}),
smartContractResults: [
new SmartContractResult({
sender: "",
receiver: "",
data: Buffer.from("@657865637574696f6e206661696c6564"),
logs: {
address: "erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x",
events: [
new TransactionEvent({
address: "erd1qqqqqqqqqqqqqpgqj8k976l59n7fyth8ujl4as5uyn3twn0ha0wsge5r5x",
identifier: "signalError",
topics: [
Address.fromBech32(
"erd1testnlersh4z0wsv8kjx39me4rmnvjkwu8dsaea7ukdvvc9z396qykv7z7",
).getPublicKey(),
Buffer.from("insufficient funds"),
],
data: Buffer.from("@657865637574696f6e206661696c6564"),
additionalData: [Buffer.from("foo"), Buffer.from("bar")],
}),
],
},
}),
],
});

assert.deepEqual(actualTransactionOutcome, expectedTransactionOutcome);
});
});
5 changes: 5 additions & 0 deletions src/interfaceOfNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ITransactionOnNetwork {
value: string;
receiver: IAddress;
sender: IAddress;
function?: string;
data: Buffer;
status: ITransactionStatus;
receipt: ITransactionReceipt;
Expand Down Expand Up @@ -63,6 +64,7 @@ export interface IContractReturnCode {
}

export interface ITransactionLogs {
address: IAddress;
events: ITransactionEvent[];

findSingleOrNoneEvent(
Expand All @@ -76,6 +78,9 @@ export interface ITransactionEvent {
readonly identifier: string;
readonly topics: ITransactionEventTopic[];
readonly data: string;
// See https://github.com/multiversx/mx-sdk-js-network-providers/blob/v2.4.0/src/transactionEvents.ts#L13
readonly dataPayload?: { valueOf(): Uint8Array };
readonly additionalData?: { valueOf(): Uint8Array }[];
Comment on lines +81 to +83
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These aren't breaking changes.


findFirstOrNoneTopic(predicate: (topic: ITransactionEventTopic) => boolean): ITransactionEventTopic | undefined;
getLastTopic(): ITransactionEventTopic;
Expand Down
Loading
Loading