From b110cd8a634ee1622bac47dd4981c3475beed01e Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 14:27:48 +0300 Subject: [PATCH 1/9] Results parser: partial fixes. --- src/errors.ts | 9 ++ src/smartcontracts/resultsParser.ts | 133 +++++++++++++----- src/smartcontracts/smartContractResults.ts | 7 +- .../wrapper/deprecatedContractResults.ts | 5 +- src/transactionLogs.ts | 50 +++++-- 5 files changed, 147 insertions(+), 57 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index b8ed438af..ed2468c44 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -108,6 +108,15 @@ export class ErrInvariantFailed extends Err { } } +/** + * Signals an unexpected condition. + */ + export class ErrUnexpectedCondition extends Err { + public constructor(message: string) { + super(`Unexpected condition: [${message}]`); + } +} + /** * Signals issues with {@link Address} instantiation. */ diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index b579933ee..90ce1dd9a 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -1,15 +1,20 @@ -import { ErrCannotParseContractResults, ErrInvariantFailed } from "../errors"; +import { ErrCannotParseContractResults } from "../errors"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { ArgSerializer } from "./argSerializer"; import { TypedOutcomeBundle, IResultsParser, UntypedOutcomeBundle } from "./interface"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; +import { SmartContractResultItem } from "./smartContractResults"; import { EndpointDefinition } from "./typesystem"; enum WellKnownEvents { OnTransactionCompleted = "completedTxEvent", - OnContractDeployment = "SCDeploy", - OnUserError = "signalError" + OnSignalError = "signalError", + OnWriteLog = "writeLog" +} + +enum WellKnownTopics { + TooMuchGas = "@too much gas provided for processing" } export class ResultsParser implements IResultsParser { @@ -49,52 +54,104 @@ export class ResultsParser implements IResultsParser { }; } - /** - * TODO: Upon further analysis, improve this function. Currently, the implementation makes some (possibly inaccurate) assumptions on the SCR & logs emission logic. - */ parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle { let resultItems = transaction.results.getAll(); - // Let's search the result holding the returnData - // (possibly inaccurate logic at this moment) - let resultItemWithReturnData = resultItems.find(item => item.nonce.valueOf() != 0); - - // If we didn't find it, then fallback to events & logs: - // (possibly inaccurate logic at this moment) - if (!resultItemWithReturnData) { - let returnCode = ReturnCode.Unknown; - - if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnTransactionCompleted)) { - // We do not extract any return data. - returnCode = ReturnCode.Ok; - } else if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnContractDeployment)) { - // When encountering this event, we assume a successful deployment. - // We do not extract any return data. - // (possibly inaccurate logic at this moment, especially in case of deployments from other contracts) - returnCode = ReturnCode.Ok; - } else if (transaction.logs.findEventByIdentifier(WellKnownEvents.OnUserError)) { - returnCode = ReturnCode.UserError; - } - - // TODO: Also handle "too much gas provided" (writeLog event) - in this case, the returnData is held in the event.data field. + let logs = transaction.logs; + // Handle simple move balances (or any other transactions without contract results / logs): + if (resultItems.length == 0 && logs.events.length == 0) { return { - returnCode: returnCode, - returnMessage: returnCode.toString(), + returnCode: ReturnCode.Unknown, + returnMessage: ReturnCode.Unknown.toString(), + values: [] + }; + } + + // Handle invalid transactions: + if (transaction.status.isInvalid()) { + return { + returnCode: ReturnCode.Unknown, + returnMessage: transaction.receipt.message, values: [] }; } - let parts = resultItemWithReturnData.getDataParts(); - let { returnCode, returnDataParts } = this.sliceDataParts(parts); + // Let's search the result holding the returnData: + let resultItemWithReturnData = this.findResultItemWithReturnData(resultItems); - return { - returnCode: returnCode, - returnMessage: returnCode.toString(), - values: returnDataParts - }; + if (resultItemWithReturnData) { + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data); + let returnMessage = resultItemWithReturnData.returnMessage || returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + // If we didn't find it, then fallback to events & logs. + + // On "signalError": + let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); + + if (eventSignalError) { + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventSignalError.data); + let lastTopic = eventSignalError.getLastTopic(); + let returnMessage = lastTopic?.toString() || returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + // On "writeLog" with topic "@too much gas provided for processing" + let eventTooMuchGas = logs.findSingleOrNoneEvent( + WellKnownEvents.OnWriteLog, + event => event.findFirstOrNoneTopic(topic => topic.toString().startsWith(WellKnownTopics.TooMuchGas)) != undefined + ); + + if (eventTooMuchGas) { + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventTooMuchGas.data); + let lastTopic = eventTooMuchGas.getLastTopic(); + let returnMessage = lastTopic?.toString() || returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + // On "writeLog" with first topic == sender + let eventWriteLogWhereTopicIsSender = logs.findSingleOrNoneEvent( + WellKnownEvents.OnWriteLog, + event => event.findFirstOrNoneTopic(topic => topic.hex() == transaction.sender.hex()) != undefined + ); + + if (eventWriteLogWhereTopicIsSender) { + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventWriteLogWhereTopicIsSender.data); + let returnMessage = returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); + } + + private findResultItemWithReturnData(items: SmartContractResultItem[]) { + let result = items.find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); + return result; } - private sliceDataParts(parts: Buffer[]): { returnCode: ReturnCode, returnDataParts: Buffer[] } { + private sliceDataFieldInParts(data: string): { returnCode: ReturnCode, returnDataParts: Buffer[] } { + let parts = new ArgSerializer().stringToBuffers(data); let emptyReturnPart = parts[0] || Buffer.from([]); let returnCodePart = parts[1] || Buffer.from([]); let returnDataParts = parts.slice(2); diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts index 4e5afc18c..54bcb437e 100644 --- a/src/smartcontracts/smartContractResults.ts +++ b/src/smartcontracts/smartContractResults.ts @@ -4,7 +4,6 @@ import { Hash } from "../hash"; import { GasLimit, GasPrice } from "../networkParams"; import { Nonce } from "../nonce"; import { TransactionHash } from "../transaction"; -import { ArgSerializer } from "./argSerializer"; export class SmartContractResults { private readonly items: SmartContractResultItem[] = []; @@ -38,6 +37,7 @@ export class SmartContractResultItem { receiver: Address = new Address(); sender: Address = new Address(); data: string = ""; + returnMessage: string = ""; previousHash: Hash = Hash.empty(); originalHash: Hash = Hash.empty(); gasLimit: GasLimit = new GasLimit(0); @@ -66,6 +66,7 @@ export class SmartContractResultItem { item.receiver = new Address(response.receiver); item.sender = new Address(response.sender); item.data = response.data || ""; + item.returnMessage = response.returnMessage || ""; item.previousHash = new TransactionHash(response.prevTxHash); item.originalHash = new TransactionHash(response.originalTxHash); item.gasLimit = new GasLimit(response.gasLimit); @@ -74,8 +75,4 @@ export class SmartContractResultItem { return item; } - - getDataParts(): Buffer[] { - return new ArgSerializer().stringToBuffers(this.data); - } } diff --git a/src/smartcontracts/wrapper/deprecatedContractResults.ts b/src/smartcontracts/wrapper/deprecatedContractResults.ts index 8b652b7b9..04f9b8df3 100644 --- a/src/smartcontracts/wrapper/deprecatedContractResults.ts +++ b/src/smartcontracts/wrapper/deprecatedContractResults.ts @@ -115,7 +115,7 @@ export class TypedResult extends SmartContractResultItem implements Result.IResu } getReturnCode(): ReturnCode { - let tokens = this.getDataParts(); + let tokens = new ArgSerializer().stringToBuffers(this.data); if (tokens.length < 2) { return ReturnCode.None; } @@ -127,7 +127,8 @@ export class TypedResult extends SmartContractResultItem implements Result.IResu this.assertSuccess(); // Skip the first 2 SCRs (eg. the @6f6b from @6f6b@2b). - return this.getDataParts().slice(2); + let dataParts = new ArgSerializer().stringToBuffers(this.data); + return dataParts.slice(2); } /** diff --git a/src/transactionLogs.ts b/src/transactionLogs.ts index f1ef9cbf9..bdc5a01b8 100644 --- a/src/transactionLogs.ts +++ b/src/transactionLogs.ts @@ -1,5 +1,5 @@ import { Address } from "./address"; -import { ErrTransactionEventNotFound } from "./errors"; +import { ErrTransactionEventNotFound, ErrUnexpectedCondition } from "./errors"; export class TransactionLogs { readonly address: Address; @@ -20,18 +20,28 @@ export class TransactionLogs { return new TransactionLogs(address, events); } - requireEventByIdentifier(identifier: string): TransactionEvent { - let event = this.findEventByIdentifier(identifier); - if (event) { - return event; + findSingleOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { + let events = this.findEvents(identifier, predicate); + + if (events.length > 1) { + throw new ErrUnexpectedCondition(`more than one event of type ${identifier}`); } - throw new ErrTransactionEventNotFound(identifier); + return events[0]; } - findEventByIdentifier(identifier: string): TransactionEvent | undefined { - let event = this.events.filter(event => event.identifier == identifier)[0]; - return event; + findFirstOrNoneEvent(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent | undefined { + return this.findEvents(identifier, predicate)[0]; + } + + findEvents(identifier: string, predicate?: (event: TransactionEvent) => boolean): TransactionEvent[] { + let events = this.events.filter(event => event.identifier == identifier); + + if (predicate) { + events = events.filter(event => predicate(event)); + } + + return events; } } @@ -39,24 +49,36 @@ export class TransactionEvent { readonly address: Address; readonly identifier: string; readonly topics: TransactionEventTopic[]; + readonly data: string; - constructor(address: Address, identifier: string, topics: TransactionEventTopic[]) { + constructor(address: Address, identifier: string, topics: TransactionEventTopic[], data: string) { this.address = address; this.identifier = identifier; this.topics = topics; + this.data = data; } static fromHttpResponse(responsePart: { address: string, identifier: string, - topics: string[] + topics: string[], + data: string }): TransactionEvent { let topics = (responsePart.topics || []).map(topic => new TransactionEventTopic(topic)); let address = new Address(responsePart.address); let identifier = responsePart.identifier || ""; - let event = new TransactionEvent(address, identifier, topics); + let data = Buffer.from(responsePart.data || "", "base64").toString(); + let event = new TransactionEvent(address, identifier, topics, data); return event; } + + findFirstOrNoneTopic(predicate: (topic: TransactionEventTopic) => boolean): TransactionEventTopic | undefined { + return this.topics.filter(topic => predicate(topic))[0]; + } + + getLastTopic(): TransactionEventTopic { + return this.topics[this.topics.length - 1]; + } } export class TransactionEventTopic { @@ -70,6 +92,10 @@ export class TransactionEventTopic { return this.raw.toString("utf8"); } + hex() { + return this.raw.toString("hex"); + } + valueOf(): Buffer { return this.raw; } From c040498b180f0ce2f4dee873bf4f35bd30246854 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 18:39:21 +0300 Subject: [PATCH 2/9] WIP. --- src/smartcontracts/resultsParser.ts | 4 ++-- src/smartcontracts/smartContractResults.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index 90ce1dd9a..1d8b781e5 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -76,7 +76,7 @@ export class ResultsParser implements IResultsParser { }; } - // Let's search the result holding the returnData: + // Let's search the contract result holding the returnData: let resultItemWithReturnData = this.findResultItemWithReturnData(resultItems); if (resultItemWithReturnData) { @@ -109,7 +109,7 @@ export class ResultsParser implements IResultsParser { // On "writeLog" with topic "@too much gas provided for processing" let eventTooMuchGas = logs.findSingleOrNoneEvent( - WellKnownEvents.OnWriteLog, + WellKnownEvents.OnWriteLog, event => event.findFirstOrNoneTopic(topic => topic.toString().startsWith(WellKnownTopics.TooMuchGas)) != undefined ); diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts index 54bcb437e..b8efb3f68 100644 --- a/src/smartcontracts/smartContractResults.ts +++ b/src/smartcontracts/smartContractResults.ts @@ -4,6 +4,7 @@ import { Hash } from "../hash"; import { GasLimit, GasPrice } from "../networkParams"; import { Nonce } from "../nonce"; import { TransactionHash } from "../transaction"; +import { TransactionLogs } from "../transactionLogs"; export class SmartContractResults { private readonly items: SmartContractResultItem[] = []; @@ -43,6 +44,7 @@ export class SmartContractResultItem { gasLimit: GasLimit = new GasLimit(0); gasPrice: GasPrice = new GasPrice(0); callType: number = 0; + logs: TransactionLogs = TransactionLogs.empty(); static fromHttpResponse(response: { hash: string, @@ -57,6 +59,7 @@ export class SmartContractResultItem { gasPrice: number, callType: number, returnMessage: string + logs: any[] }): SmartContractResultItem { let item = new SmartContractResultItem(); @@ -73,6 +76,8 @@ export class SmartContractResultItem { item.gasPrice = new GasPrice(response.gasPrice); item.callType = response.callType; + item.logs = TransactionLogs.fromHttpResponse(response.logs || {}); + return item; } } From 7c9b6ba319729edae49a0ae527d15dc291f73415 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 21:09:21 +0300 Subject: [PATCH 3/9] Adjust parser. --- src/smartcontracts/resultsParser.ts | 157 ++++++++++++++++++---------- 1 file changed, 100 insertions(+), 57 deletions(-) diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index 1d8b781e5..c6e39bfbf 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -1,10 +1,12 @@ +import { Address } from "../address"; import { ErrCannotParseContractResults } from "../errors"; +import { TransactionLogs } from "../transactionLogs"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { ArgSerializer } from "./argSerializer"; import { TypedOutcomeBundle, IResultsParser, UntypedOutcomeBundle } from "./interface"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; -import { SmartContractResultItem } from "./smartContractResults"; +import { SmartContractResults } from "./smartContractResults"; import { EndpointDefinition } from "./typesystem"; enum WellKnownEvents { @@ -55,11 +57,46 @@ export class ResultsParser implements IResultsParser { } parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle { - let resultItems = transaction.results.getAll(); - let logs = transaction.logs; + let bundle: UntypedOutcomeBundle | null; - // Handle simple move balances (or any other transactions without contract results / logs): - if (resultItems.length == 0 && logs.events.length == 0) { + bundle = this.createBundleOnSimpleMoveBalance(transaction) + if (bundle) { + return bundle; + } + + bundle = this.createBundleOnInvalidTransaction(transaction); + if (bundle) { + return bundle; + } + + bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.results); + if (bundle) { + return bundle; + } + + bundle = this.createBundleOnSignalError(transaction.logs); + if (bundle) { + return bundle; + } + + bundle = this.createBundleOnTooMuchGasWarning(transaction.logs); + if (bundle) { + return bundle; + } + + bundle = this.createBundleOnWriteLogWhereFirstTopicEqualsAddress(transaction.logs, transaction.sender); + if (bundle) { + return bundle; + } + + throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); + } + + private createBundleOnSimpleMoveBalance(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { + let noResults = transaction.results.getAll().length == 0; + let noLogs = transaction.logs.events.length == 0; + + if (noResults && noLogs) { return { returnCode: ReturnCode.Unknown, returnMessage: ReturnCode.Unknown.toString(), @@ -67,7 +104,10 @@ export class ResultsParser implements IResultsParser { }; } - // Handle invalid transactions: + return null; + } + + private createBundleOnInvalidTransaction(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { if (transaction.status.isInvalid()) { return { returnCode: ReturnCode.Unknown, @@ -76,78 +116,81 @@ export class ResultsParser implements IResultsParser { }; } - // Let's search the contract result holding the returnData: - let resultItemWithReturnData = this.findResultItemWithReturnData(resultItems); - - if (resultItemWithReturnData) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data); - let returnMessage = resultItemWithReturnData.returnMessage || returnCode.toString(); + return null; + } - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts - }; + private createBundleOnEasilyFoundResultWithReturnData(results: SmartContractResults): UntypedOutcomeBundle | null { + let resultItemWithReturnData = results.getAll().find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); + if (!resultItemWithReturnData) { + return null; } - // If we didn't find it, then fallback to events & logs. - - // On "signalError": - let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data); + let returnMessage = resultItemWithReturnData.returnMessage || returnCode.toString(); - if (eventSignalError) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventSignalError.data); - let lastTopic = eventSignalError.getLastTopic(); - let returnMessage = lastTopic?.toString() || returnCode.toString(); + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts - }; + private createBundleOnSignalError(logs: TransactionLogs): UntypedOutcomeBundle | null { + let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); + if (!eventSignalError) { + return null; } - // On "writeLog" with topic "@too much gas provided for processing" + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventSignalError.data); + let lastTopic = eventSignalError.getLastTopic(); + let returnMessage = lastTopic?.toString() || returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + private createBundleOnTooMuchGasWarning(logs: TransactionLogs): UntypedOutcomeBundle | null { let eventTooMuchGas = logs.findSingleOrNoneEvent( WellKnownEvents.OnWriteLog, event => event.findFirstOrNoneTopic(topic => topic.toString().startsWith(WellKnownTopics.TooMuchGas)) != undefined ); - if (eventTooMuchGas) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventTooMuchGas.data); - let lastTopic = eventTooMuchGas.getLastTopic(); - let returnMessage = lastTopic?.toString() || returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts - }; + if (!eventTooMuchGas) { + return null; } - // On "writeLog" with first topic == sender + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventTooMuchGas.data); + let lastTopic = eventTooMuchGas.getLastTopic(); + let returnMessage = lastTopic?.toString() || returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + private createBundleOnWriteLogWhereFirstTopicEqualsAddress(logs: TransactionLogs, address: Address): UntypedOutcomeBundle | null { let eventWriteLogWhereTopicIsSender = logs.findSingleOrNoneEvent( WellKnownEvents.OnWriteLog, - event => event.findFirstOrNoneTopic(topic => topic.hex() == transaction.sender.hex()) != undefined + event => event.findFirstOrNoneTopic(topic => topic.hex() == address.hex()) != undefined ); - if (eventWriteLogWhereTopicIsSender) { - let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventWriteLogWhereTopicIsSender.data); - let returnMessage = returnCode.toString(); - - return { - returnCode: returnCode, - returnMessage: returnMessage, - values: returnDataParts - }; + if (!eventWriteLogWhereTopicIsSender) { + return null; } - throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); - } + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventWriteLogWhereTopicIsSender.data); + let returnMessage = returnCode.toString(); - private findResultItemWithReturnData(items: SmartContractResultItem[]) { - let result = items.find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); - return result; + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; } private sliceDataFieldInParts(data: string): { returnCode: ReturnCode, returnDataParts: Buffer[] } { From 6f5e426ab3fdffabb993b21ef301bbc81789bb36 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 21:57:39 +0300 Subject: [PATCH 4/9] Added some tests. --- src/smartcontracts/resultsParser.spec.ts | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts index 1332f8ce7..03181923c 100644 --- a/src/smartcontracts/resultsParser.spec.ts +++ b/src/smartcontracts/resultsParser.spec.ts @@ -7,6 +7,9 @@ import { ResultsParser } from "./resultsParser"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { SmartContractResultItem, SmartContractResults } from "./smartContractResults"; import { Nonce } from "../nonce"; +import { TransactionEvent, TransactionEventTopic, TransactionLogs } from "../transactionLogs"; +import { Address } from "../address"; + describe("test smart contract results parser", () => { let parser = new ResultsParser(); @@ -57,4 +60,67 @@ describe("test smart contract results parser", () => { assert.deepEqual(bundle.secondValue, BytesValue.fromHex("abba")); assert.lengthOf(bundle.values, 2); }); + + it("should parse contract outcome, on easily found result with return data", async () => { + let transaction = new TransactionOnNetwork({ + results: new SmartContractResults([ + new SmartContractResultItem({ + nonce: new Nonce(42), + data: "@6f6b@03", + returnMessage: "foobar" + }) + ]) + }); + + let bundle = parser.parseUntypedOutcome(transaction); + assert.deepEqual(bundle.returnCode, ReturnCode.Ok); + assert.equal(bundle.returnMessage, "foobar"); + assert.deepEqual(bundle.values, [Buffer.from("03", "hex")]); + }); + + it("should parse contract outcome, on signal error", async () => { + let transaction = new TransactionOnNetwork({ + logs: new TransactionLogs( + new Address(), + [ + new TransactionEvent( + new Address(), + "signalError", + [ + new TransactionEventTopic(Buffer.from("something happened").toString("base64")) + ], + `@${Buffer.from("user error").toString("hex")}@07` + ) + ] + ) + }); + + let bundle = parser.parseUntypedOutcome(transaction); + assert.deepEqual(bundle.returnCode, ReturnCode.UserError); + assert.equal(bundle.returnMessage, "something happened"); + assert.deepEqual(bundle.values, [Buffer.from("07", "hex")]); + }); + + it("should parse contract outcome, on too much gas warning", async () => { + let transaction = new TransactionOnNetwork({ + logs: new TransactionLogs( + new Address(), + [ + new TransactionEvent( + new Address(), + "writeLog", + [ + new TransactionEventTopic("QHRvbyBtdWNoIGdhcyBwcm92aWRlZCBmb3IgcHJvY2Vzc2luZzogZ2FzIHByb3ZpZGVkID0gNTk2Mzg0NTAwLCBnYXMgdXNlZCA9IDczMzAxMA==") + ], + Buffer.from("QDZmNmI=", "base64").toString() + ) + ] + ) + }); + + let bundle = parser.parseUntypedOutcome(transaction); + assert.deepEqual(bundle.returnCode, ReturnCode.Ok); + assert.equal(bundle.returnMessage, "@too much gas provided for processing: gas provided = 596384500, gas used = 733010"); + assert.deepEqual(bundle.values, []); + }); }); From b7f673cabeb095fc482f79228cf6bce14d8a15d2 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 22:15:23 +0300 Subject: [PATCH 5/9] Parse transaction metadata (use transaction-decoder). Allow one to override the parser. --- package.json | 1 + src/smartcontracts/resultsParser.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/package.json b/package.json index cb2072e27..848a86008 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "author": "ElrondNetwork", "license": "GPL-3.0-or-later", "dependencies": { + "@elrondnetwork/transaction-decoder": "0.1.0", "abort-controller": "3.0.0", "axios": "0.24.0", "bech32": "1.1.4", diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index c6e39bfbf..3387fa659 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -1,3 +1,4 @@ +import { TransactionDecoder, TransactionMetadata } from "@elrondnetwork/transaction-decoder"; import { Address } from "../address"; import { ErrCannotParseContractResults } from "../errors"; import { TransactionLogs } from "../transactionLogs"; @@ -59,6 +60,8 @@ export class ResultsParser implements IResultsParser { parseUntypedOutcome(transaction: TransactionOnNetwork): UntypedOutcomeBundle { let bundle: UntypedOutcomeBundle | null; + let transactionMetadata = this.parseTransactionMetadata(transaction); + bundle = this.createBundleOnSimpleMoveBalance(transaction) if (bundle) { return bundle; @@ -89,9 +92,24 @@ export class ResultsParser implements IResultsParser { return bundle; } + bundle = this.createBundleCustom(transaction, transactionMetadata); + if (bundle) { + return bundle; + } + throw new ErrCannotParseContractResults(`transaction ${transaction.hash.toString()}`); } + private parseTransactionMetadata(transaction: TransactionOnNetwork): TransactionMetadata { + return new TransactionDecoder().getTransactionMetadata({ + sender: transaction.sender.bech32(), + receiver: transaction.receiver.bech32(), + data: transaction.data.encoded(), + value: transaction.value.toString(), + type: transaction.type.value + }) + } + private createBundleOnSimpleMoveBalance(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { let noResults = transaction.results.getAll().length == 0; let noLogs = transaction.logs.events.length == 0; @@ -193,6 +211,13 @@ export class ResultsParser implements IResultsParser { }; } + /** + * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are failing. + */ + protected createBundleCustom(_transaction: TransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + return null; + } + private sliceDataFieldInParts(data: string): { returnCode: ReturnCode, returnDataParts: Buffer[] } { let parts = new ArgSerializer().stringToBuffers(data); let emptyReturnPart = parts[0] || Buffer.from([]); From 703d023efb297ff9e07fe02ab5168d28fa086c94 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 23:22:10 +0300 Subject: [PATCH 6/9] Add extra tests (based on real-world samples). Improve parser. --- src/smartcontracts/resultsParser.spec.ts | 58 ++++++++++++++++++++++ src/smartcontracts/resultsParser.ts | 61 +++++++++++++++++++++--- src/transactionOnNetwork.ts | 4 +- 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts index 03181923c..6a697497a 100644 --- a/src/smartcontracts/resultsParser.spec.ts +++ b/src/smartcontracts/resultsParser.spec.ts @@ -1,3 +1,5 @@ +import * as fs from "fs"; +import path from "path"; import { assert } from "chai"; import { BigUIntType, BigUIntValue, EndpointDefinition, EndpointModifiers, EndpointParameterDefinition } from "./typesystem"; import { BytesType, BytesValue } from "./typesystem/bytes"; @@ -7,9 +9,24 @@ import { ResultsParser } from "./resultsParser"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { SmartContractResultItem, SmartContractResults } from "./smartContractResults"; import { Nonce } from "../nonce"; +import { TransactionHash } from "../transaction"; import { TransactionEvent, TransactionEventTopic, TransactionLogs } from "../transactionLogs"; import { Address } from "../address"; +import { Logger, LogLevel } from "../logger"; +const KnownReturnCodes: string[] = [ + ReturnCode.None.valueOf(), + ReturnCode.Ok.valueOf(), + ReturnCode.FunctionNotFound.valueOf(), + ReturnCode.FunctionWrongSignature.valueOf(), + ReturnCode.ContractNotFound.valueOf(), + ReturnCode.UserError.valueOf(), + ReturnCode.OutOfGas.valueOf(), + ReturnCode.AccountCollision.valueOf(), + ReturnCode.OutOfFunds.valueOf(), + ReturnCode.CallStackOverFlow.valueOf(), ReturnCode.ContractInvalid.valueOf(), + ReturnCode.ExecutionFailed.valueOf() +]; describe("test smart contract results parser", () => { let parser = new ResultsParser(); @@ -123,4 +140,45 @@ describe("test smart contract results parser", () => { assert.equal(bundle.returnMessage, "@too much gas provided for processing: gas provided = 596384500, gas used = 733010"); assert.deepEqual(bundle.values, []); }); + + it.only("should parse real-world contract outcomes", async () => { + let oldLogLevel = Logger.logLevel; + Logger.setLevel(LogLevel.Trace); + + let folder = path.resolve(process.env["SAMPLES"] || "SAMPLES") + let samples = loadRealWorldSamples(folder); + + for (const [transaction, _] of samples) { + console.log("Transaction:", transaction.hash.toString()); + + let bundle = parser.parseUntypedOutcome(transaction); + + console.log("Return code:", bundle.returnCode.toString()); + console.log("Return message:", bundle.returnMessage); + console.log("Num values:", bundle.values.length); + console.log("=".repeat(80)); + + assert.include(KnownReturnCodes, bundle.returnCode.valueOf()); + } + + Logger.setLevel(oldLogLevel); + }); + + function loadRealWorldSamples(folder: string): [TransactionOnNetwork, string][] { + let transactionFiles = fs.readdirSync(folder); + let samples: [TransactionOnNetwork, string][] = []; + + for (const file of transactionFiles) { + let txHash = new TransactionHash(path.basename(file, ".json")); + let filePath = path.resolve(folder, file); + let jsonContent: string = fs.readFileSync(filePath, { encoding: "utf8" }); + let json = JSON.parse(jsonContent); + let payload = json["data"]["transaction"]; + let transaction = TransactionOnNetwork.fromHttpResponse(txHash, payload); + + samples.push([transaction, jsonContent]); + } + + return samples; + } }); diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index 3387fa659..1b04a9696 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -1,6 +1,7 @@ import { TransactionDecoder, TransactionMetadata } from "@elrondnetwork/transaction-decoder"; import { Address } from "../address"; import { ErrCannotParseContractResults } from "../errors"; +import { Logger } from "../logger"; import { TransactionLogs } from "../transactionLogs"; import { TransactionOnNetwork } from "../transactionOnNetwork"; import { ArgSerializer } from "./argSerializer"; @@ -20,6 +21,10 @@ enum WellKnownTopics { TooMuchGas = "@too much gas provided for processing" } +/** + * Parses contract query responses and smart contract results. + * The parsing involves some heuristics, in order to handle slight inconsistencies (e.g. some SCRs are present on API, but missing on Gateway). + */ export class ResultsParser implements IResultsParser { parseQueryResponse(queryResponse: QueryResponse, endpoint: EndpointDefinition): TypedOutcomeBundle { let parts = queryResponse.getReturnDataParts(); @@ -64,36 +69,49 @@ export class ResultsParser implements IResultsParser { bundle = this.createBundleOnSimpleMoveBalance(transaction) if (bundle) { + Logger.trace("parseUntypedOutcome(): on simple move balance"); return bundle; } bundle = this.createBundleOnInvalidTransaction(transaction); if (bundle) { + Logger.trace("parseUntypedOutcome(): on invalid transaction"); return bundle; } bundle = this.createBundleOnEasilyFoundResultWithReturnData(transaction.results); if (bundle) { + Logger.trace("parseUntypedOutcome(): on easily found result with return data"); return bundle; } bundle = this.createBundleOnSignalError(transaction.logs); if (bundle) { + Logger.trace("parseUntypedOutcome(): on signal error"); return bundle; } bundle = this.createBundleOnTooMuchGasWarning(transaction.logs); if (bundle) { + Logger.trace("parseUntypedOutcome(): on 'too much gas' warning"); return bundle; } bundle = this.createBundleOnWriteLogWhereFirstTopicEqualsAddress(transaction.logs, transaction.sender); if (bundle) { + Logger.trace("parseUntypedOutcome(): on writelog with topics[0] == tx.sender"); return bundle; } - bundle = this.createBundleCustom(transaction, transactionMetadata); + bundle = this.createBundleWithCustomHeuristics(transaction, transactionMetadata); if (bundle) { + Logger.trace("parseUntypedOutcome(): with custom heuristics"); + return bundle; + } + + bundle = this.createBundleWithFallbackHeuristics(transaction, transactionMetadata); + if (bundle) { + Logger.trace("parseUntypedOutcome(): with fallback heuristics"); return bundle; } @@ -127,11 +145,15 @@ export class ResultsParser implements IResultsParser { private createBundleOnInvalidTransaction(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { if (transaction.status.isInvalid()) { - return { - returnCode: ReturnCode.Unknown, - returnMessage: transaction.receipt.message, - values: [] - }; + if (transaction.receipt.data) { + return { + returnCode: ReturnCode.OutOfFunds, + returnMessage: transaction.receipt.data, + values: [] + }; + } + + // If there's no receipt message, let other heuristics to handle the outcome (most probably, a log with "signalError" is emitted). } return null; @@ -214,7 +236,32 @@ export class ResultsParser implements IResultsParser { /** * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are failing. */ - protected createBundleCustom(_transaction: TransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + protected createBundleWithCustomHeuristics(_transaction: TransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + return null; + } + + private createBundleWithFallbackHeuristics(transaction: TransactionOnNetwork, transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { + let contractAddress = new Address(transactionMetadata.receiver); + + for (const resultItem of transaction.results.getAll()) { + let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, event => { + let addressIsSender = event.address.equals(transaction.sender); + let firstTopicIsContract = event.topics[0]?.hex() == contractAddress.hex(); + return addressIsSender && firstTopicIsContract; + }); + + if (writeLogWithReturnData) { + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(writeLogWithReturnData.data); + let returnMessage = returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + } + return null; } diff --git a/src/transactionOnNetwork.ts b/src/transactionOnNetwork.ts index 4cd75c206..134f98f4b 100644 --- a/src/transactionOnNetwork.ts +++ b/src/transactionOnNetwork.ts @@ -109,7 +109,7 @@ export class TransactionOnNetworkType { export class Receipt { value: Balance = Balance.Zero(); sender: Address = new Address(); - message: string = ""; + data: string = ""; hash: TransactionHash = TransactionHash.empty(); static fromHttpResponse(response: { @@ -122,7 +122,7 @@ export class Receipt { receipt.value = Balance.fromString(response.value); receipt.sender = new Address(response.sender); - receipt.message = response.data; + receipt.data = response.data; receipt.hash = new TransactionHash(response.txHash); return receipt; From e79229653358a481c98c3050456403bd3797b385 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sun, 3 Apr 2022 23:45:30 +0300 Subject: [PATCH 7/9] Fixes and improvements. --- src/smartcontracts/resultsParser.spec.ts | 9 ++++++-- src/smartcontracts/resultsParser.ts | 26 ++++++++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts index 6a697497a..79cc97ff2 100644 --- a/src/smartcontracts/resultsParser.spec.ts +++ b/src/smartcontracts/resultsParser.spec.ts @@ -25,7 +25,12 @@ const KnownReturnCodes: string[] = [ ReturnCode.AccountCollision.valueOf(), ReturnCode.OutOfFunds.valueOf(), ReturnCode.CallStackOverFlow.valueOf(), ReturnCode.ContractInvalid.valueOf(), - ReturnCode.ExecutionFailed.valueOf() + ReturnCode.ExecutionFailed.valueOf(), + // Provided by protocol, not by VM: + "insufficient funds", + "operation in account not permitted not the owner of the account", + "sending value to non payable contract", + "invalid receiver address" ]; describe("test smart contract results parser", () => { @@ -141,7 +146,7 @@ describe("test smart contract results parser", () => { assert.deepEqual(bundle.values, []); }); - it.only("should parse real-world contract outcomes", async () => { + it.skip("should parse real-world contract outcomes", async () => { let oldLogLevel = Logger.logLevel; Logger.setLevel(LogLevel.Trace); diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index 1b04a9696..d76173e5f 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -134,8 +134,8 @@ export class ResultsParser implements IResultsParser { if (noResults && noLogs) { return { - returnCode: ReturnCode.Unknown, - returnMessage: ReturnCode.Unknown.toString(), + returnCode: ReturnCode.None, + returnMessage: ReturnCode.None.toString(), values: [] }; } @@ -243,6 +243,7 @@ export class ResultsParser implements IResultsParser { private createBundleWithFallbackHeuristics(transaction: TransactionOnNetwork, transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { let contractAddress = new Address(transactionMetadata.receiver); + // Search the nested logs for matching events (writeLog): for (const resultItem of transaction.results.getAll()) { let writeLogWithReturnData = resultItem.logs.findSingleOrNoneEvent(WellKnownEvents.OnWriteLog, event => { let addressIsSender = event.address.equals(transaction.sender); @@ -266,14 +267,21 @@ export class ResultsParser implements IResultsParser { } private sliceDataFieldInParts(data: string): { returnCode: ReturnCode, returnDataParts: Buffer[] } { - let parts = new ArgSerializer().stringToBuffers(data); - let emptyReturnPart = parts[0] || Buffer.from([]); - let returnCodePart = parts[1] || Buffer.from([]); - let returnDataParts = parts.slice(2); - - if (emptyReturnPart.length != 0) { - throw new ErrCannotParseContractResults("no leading empty part"); + // By default, skip the first part, which is usually empty (e.g. "[empty]@6f6b") + let startingIndex = 1; + + // Before trying to parse the hex strings, cut the unwanted parts of the data field, in case of token transfers: + if (data.startsWith("ESDTTransfer")) { + // Skip "ESDTTransfer" (1), token identifier (2), amount (3) + startingIndex = 3; + } else { + // TODO: Upon gathering more transaction samples, fix for other kinds of transfers, as well. } + + let parts = new ArgSerializer().stringToBuffers(data); + let returnCodePart = parts[startingIndex] || Buffer.from([]); + let returnDataParts = parts.slice(startingIndex + 1); + if (returnCodePart.length == 0) { throw new ErrCannotParseContractResults("no return code"); } From 1e884d5a88e5bdebbefe94af579936d2ed5f153c Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 4 Apr 2022 16:08:29 +0300 Subject: [PATCH 8/9] Fix after merge. Update package-lock. --- package-lock.json | 29 ++++++++++++++++++++++++++++ src/transactionCompletionStrategy.ts | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1f0386255..b2fdf5979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "10.0.0-beta.3", "license": "GPL-3.0-or-later", "dependencies": { + "@elrondnetwork/transaction-decoder": "0.1.0", "abort-controller": "3.0.0", "axios": "0.24.0", "bech32": "1.1.4", @@ -466,6 +467,19 @@ "node": ">=10.0.0" } }, + "node_modules/@elrondnetwork/transaction-decoder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@elrondnetwork/transaction-decoder/-/transaction-decoder-0.1.0.tgz", + "integrity": "sha512-R9YSiJCAgdlSzTKBy7/KjohFghmUhZIGDqWt6NGXrf33EN24QB15Q0LgHEW7lXsqsDSF5GpRYetsS7V3jYZQOg==", + "dependencies": { + "bech32": "^2.0.0" + } + }, + "node_modules/@elrondnetwork/transaction-decoder/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "node_modules/@goto-bus-stop/common-shake": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@goto-bus-stop/common-shake/-/common-shake-2.4.0.tgz", @@ -6022,6 +6036,21 @@ } } }, + "@elrondnetwork/transaction-decoder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@elrondnetwork/transaction-decoder/-/transaction-decoder-0.1.0.tgz", + "integrity": "sha512-R9YSiJCAgdlSzTKBy7/KjohFghmUhZIGDqWt6NGXrf33EN24QB15Q0LgHEW7lXsqsDSF5GpRYetsS7V3jYZQOg==", + "requires": { + "bech32": "^2.0.0" + }, + "dependencies": { + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + } + } + }, "@goto-bus-stop/common-shake": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@goto-bus-stop/common-shake/-/common-shake-2.4.0.tgz", diff --git a/src/transactionCompletionStrategy.ts b/src/transactionCompletionStrategy.ts index e52a12f31..6573d5f00 100644 --- a/src/transactionCompletionStrategy.ts +++ b/src/transactionCompletionStrategy.ts @@ -29,7 +29,7 @@ export class TransactionCompletionStrategy { // Handle gateway mechanics: for (const completionEvent of WellKnownCompletionEvents) { - if (transaction.logs.findEventByIdentifier(completionEvent)) { + if (transaction.logs.findFirstOrNoneEvent(completionEvent)) { // Certainly completed. console.debug("TransactionCompletionStrategy.isCompleted(), found event:", completionEvent); return true; From e73c0779446d92416a1a2e751a1da17146919827 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 4 Apr 2022 16:17:51 +0300 Subject: [PATCH 9/9] Fix after self-review. --- CHANGELOG.md | 2 ++ src/smartcontracts/resultsParser.spec.ts | 4 +++- src/smartcontracts/resultsParser.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a604339..272bddf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## Unreleased - [Breaking change: adjustements to transaction awaitening and completion, transaction watcher](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/173) - [Breaking change: simplify network config / improve design - not a singleton anymore](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/176) + - [Fix / improve results parser (better heuristics)](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/177) **Breaking changes** - Removed utility functions: `transaction.awaitExecuted()`, `transaction.awaitPending()`. `TransactionWatcher` should be used directly, instead. @@ -17,6 +18,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Removed `NetworkConfig.getDefault()` and `NetworkConfig.sync()`. Instead, one should use `let networkConfig = await provider.getNetworkConfig()`. - Constructor of `Transaction` now requires `chainID`, as well. - Added `Interaction.withChainID()` - must be used before calling `buildTransaction()`. + - Altered a bit the public interface of `TransactionEvent`, `Receipt` (renamed fields, renamed methods). ## [10.0.0-beta.3] - [Extract dapp / signing providers to separate repositories](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/170) diff --git a/src/smartcontracts/resultsParser.spec.ts b/src/smartcontracts/resultsParser.spec.ts index 79cc97ff2..e87e1fa87 100644 --- a/src/smartcontracts/resultsParser.spec.ts +++ b/src/smartcontracts/resultsParser.spec.ts @@ -26,7 +26,7 @@ const KnownReturnCodes: string[] = [ ReturnCode.OutOfFunds.valueOf(), ReturnCode.CallStackOverFlow.valueOf(), ReturnCode.ContractInvalid.valueOf(), ReturnCode.ExecutionFailed.valueOf(), - // Provided by protocol, not by VM: + // Returned by protocol, not by VM: "insufficient funds", "operation in account not permitted not the owner of the account", "sending value to non payable contract", @@ -146,6 +146,8 @@ describe("test smart contract results parser", () => { assert.deepEqual(bundle.values, []); }); + // 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 () => { let oldLogLevel = Logger.logLevel; Logger.setLevel(LogLevel.Trace); diff --git a/src/smartcontracts/resultsParser.ts b/src/smartcontracts/resultsParser.ts index d76173e5f..96e3d7a65 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -234,7 +234,7 @@ export class ResultsParser implements IResultsParser { } /** - * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are failing. + * Override this method (in a subclass of {@link ResultsParser}) if the basic heuristics of the parser are not sufficient. */ protected createBundleWithCustomHeuristics(_transaction: TransactionOnNetwork, _transactionMetadata: TransactionMetadata): UntypedOutcomeBundle | null { return null; @@ -275,7 +275,7 @@ export class ResultsParser implements IResultsParser { // Skip "ESDTTransfer" (1), token identifier (2), amount (3) startingIndex = 3; } else { - // TODO: Upon gathering more transaction samples, fix for other kinds of transfers, as well. + // TODO: Upon gathering more transaction samples, fix for other kinds of transfers, as well (future PR, as needed). } let parts = new ArgSerializer().stringToBuffers(data);