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/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/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/errors.ts b/src/errors.ts index abf39260d..8a50ca538 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.spec.ts b/src/smartcontracts/resultsParser.spec.ts index 1332f8ce7..e87e1fa87 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,6 +9,29 @@ 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(), + // 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", + "invalid receiver address" +]; describe("test smart contract results parser", () => { let parser = new ResultsParser(); @@ -57,4 +82,110 @@ 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, []); + }); + + // 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); + + 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 b579933ee..96e3d7a65 100644 --- a/src/smartcontracts/resultsParser.ts +++ b/src/smartcontracts/resultsParser.ts @@ -1,17 +1,30 @@ -import { ErrCannotParseContractResults, ErrInvariantFailed } from "../errors"; +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"; import { TypedOutcomeBundle, IResultsParser, UntypedOutcomeBundle } from "./interface"; import { QueryResponse } from "./queryResponse"; import { ReturnCode } from "./returnCode"; +import { SmartContractResults } 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" +} + +/** + * 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(); @@ -49,59 +62,226 @@ 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); + let bundle: UntypedOutcomeBundle | null; - // 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; - } + let transactionMetadata = this.parseTransactionMetadata(transaction); + + 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.createBundleWithCustomHeuristics(transaction, transactionMetadata); + if (bundle) { + Logger.trace("parseUntypedOutcome(): with custom heuristics"); + return bundle; + } - // TODO: Also handle "too much gas provided" (writeLog event) - in this case, the returnData is held in the event.data field. + bundle = this.createBundleWithFallbackHeuristics(transaction, transactionMetadata); + if (bundle) { + Logger.trace("parseUntypedOutcome(): with fallback heuristics"); + 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; + + if (noResults && noLogs) { return { - returnCode: returnCode, - returnMessage: returnCode.toString(), + returnCode: ReturnCode.None, + returnMessage: ReturnCode.None.toString(), values: [] }; } - let parts = resultItemWithReturnData.getDataParts(); - let { returnCode, returnDataParts } = this.sliceDataParts(parts); + return null; + } + + private createBundleOnInvalidTransaction(transaction: TransactionOnNetwork): UntypedOutcomeBundle | null { + if (transaction.status.isInvalid()) { + 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; + } + + private createBundleOnEasilyFoundResultWithReturnData(results: SmartContractResults): UntypedOutcomeBundle | null { + let resultItemWithReturnData = results.getAll().find(item => item.nonce.valueOf() != 0 && item.data.startsWith("@")); + if (!resultItemWithReturnData) { + return null; + } + + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(resultItemWithReturnData.data); + let returnMessage = resultItemWithReturnData.returnMessage || returnCode.toString(); return { returnCode: returnCode, - returnMessage: returnCode.toString(), + returnMessage: returnMessage, values: returnDataParts }; } - private sliceDataParts(parts: Buffer[]): { returnCode: ReturnCode, returnDataParts: Buffer[] } { - let emptyReturnPart = parts[0] || Buffer.from([]); - let returnCodePart = parts[1] || Buffer.from([]); - let returnDataParts = parts.slice(2); + private createBundleOnSignalError(logs: TransactionLogs): UntypedOutcomeBundle | null { + let eventSignalError = logs.findSingleOrNoneEvent(WellKnownEvents.OnSignalError); + if (!eventSignalError) { + return null; + } - if (emptyReturnPart.length != 0) { - throw new ErrCannotParseContractResults("no leading empty part"); + 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) { + return null; + } + + 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() == address.hex()) != undefined + ); + + if (!eventWriteLogWhereTopicIsSender) { + return null; } + + let { returnCode, returnDataParts } = this.sliceDataFieldInParts(eventWriteLogWhereTopicIsSender.data); + let returnMessage = returnCode.toString(); + + return { + returnCode: returnCode, + returnMessage: returnMessage, + values: returnDataParts + }; + } + + /** + * 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; + } + + 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); + 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; + } + + private sliceDataFieldInParts(data: string): { returnCode: ReturnCode, returnDataParts: Buffer[] } { + // 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 (future PR, as needed). + } + + 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"); } diff --git a/src/smartcontracts/smartContractResults.ts b/src/smartcontracts/smartContractResults.ts index 6272dc3c2..e6896accb 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"; import { TransactionLogs } from "../transactionLogs"; export class SmartContractResults { @@ -39,6 +38,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); @@ -69,6 +69,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); @@ -79,8 +80,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/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; 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; } diff --git a/src/transactionOnNetwork.ts b/src/transactionOnNetwork.ts index 9eac72c0a..50c918aca 100644 --- a/src/transactionOnNetwork.ts +++ b/src/transactionOnNetwork.ts @@ -134,7 +134,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: { @@ -147,7 +147,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;