diff --git a/src/RecordStatusError.js b/src/RecordStatusError.js new file mode 100644 index 000000000..1ff1daa30 --- /dev/null +++ b/src/RecordStatusError.js @@ -0,0 +1,48 @@ +/*- + * ‌ + * Hedera JavaScript SDK + * ​ + * Copyright (C) 2020 - 2022 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +import StatusError from "./StatusError.js"; + +/** + * @typedef {import("./Status.js").default} Status + * @typedef {import("./transaction/TransactionId.js").default} TransactionId + * @typedef {import("./transaction/TransactionRecord").default} TransactionRecord + */ + +export default class RecordStatusError extends StatusError { + /** + * @param {object} props + * @param {TransactionRecord} props.transactionRecord + * @param {Status} props.status + * @param {TransactionId} props.transactionId + */ + constructor(props) { + super( + props, + `Record for transaction ${props.transactionId.toString()} contained error status ${props.status.toString()}` + ); + + /** + * @type {TransactionRecord} + * @readonly + */ + this.transactionRecord = props.transactionRecord; + } +} diff --git a/src/transaction/TransactionRecordQuery.js b/src/transaction/TransactionRecordQuery.js index 6dedc6562..6a41f1757 100644 --- a/src/transaction/TransactionRecordQuery.js +++ b/src/transaction/TransactionRecordQuery.js @@ -25,6 +25,7 @@ import TransactionId from "./TransactionId.js"; import Status from "../Status.js"; import PrecheckStatusError from "../PrecheckStatusError.js"; import ReceiptStatusError from "../ReceiptStatusError.js"; +import RecordStatusError from "../RecordStatusError.js"; import { ExecutionState } from "../Executable.js"; import * as HashgraphProto from "@hashgraph/proto"; @@ -288,12 +289,21 @@ export default class TransactionRecordQuery extends Query { ? nodeTransactionPrecheckCode : proto.ResponseCodeEnum.OK ); - switch (status) { case Status.Ok: // Do nothing break; + case Status.ContractRevertExecuted: + return new RecordStatusError({ + status, + transactionId: this._getTransactionId(), + transactionRecord: TransactionRecord._fromProtobuf({ + transactionRecord: + response.transactionGetRecord?.transactionRecord, + }), + }); + default: return new PrecheckStatusError({ status, @@ -320,11 +330,26 @@ export default class TransactionRecordQuery extends Query { status = Status._fromCode(receiptStatusError); - return new ReceiptStatusError({ - status, - transactionId: this._getTransactionId(), - transactionReceipt: TransactionReceipt._fromProtobuf({ receipt }), - }); + switch (status) { + case Status.ContractRevertExecuted: + return new RecordStatusError({ + status, + transactionId: this._getTransactionId(), + transactionRecord: TransactionRecord._fromProtobuf({ + transactionRecord: + response.transactionGetRecord?.transactionRecord, + }), + }); + + default: + return new ReceiptStatusError({ + status, + transactionId: this._getTransactionId(), + transactionReceipt: TransactionReceipt._fromProtobuf({ + receipt, + }), + }); + } } /** diff --git a/src/transaction/TransactionResponse.js b/src/transaction/TransactionResponse.js index 7ceb2c283..91ac022c6 100644 --- a/src/transaction/TransactionResponse.js +++ b/src/transaction/TransactionResponse.js @@ -92,6 +92,8 @@ export default class TransactionResponse { } /** + * getRecord is calling getReceipt and in case the receipt status code is not OK, only the receipt is returned. + * * @param {Client} client * @returns {Promise} */ @@ -101,6 +103,22 @@ export default class TransactionResponse { return this.getRecordQuery().execute(client); } + /** + * getVerboseRecord is calling getReceipt and in case the receipt status code is not OK, the record is returned. + * + * @param {Client} client + * @returns {Promise} + */ + async getVerboseRecord(client) { + try { + // The receipt needs to be called in order to wait for transaction to be included in the consensus. Otherwise we are going to get "DUPLICATE_TRANSACTION". + await this.getReceiptQuery().execute(client); + return this.getRecordQuery().execute(client); + } catch (e) { + return this.getRecordQuery().execute(client); + } + } + /** * @param {Signer} signer * @returns {Promise} diff --git a/test/integration/TransactionRecordIntegrationTest.js b/test/integration/TransactionRecordIntegrationTest.js new file mode 100644 index 000000000..4bbc9aceb --- /dev/null +++ b/test/integration/TransactionRecordIntegrationTest.js @@ -0,0 +1,89 @@ +import { + FileCreateTransaction, + ContractCreateTransaction, + FileAppendTransaction, + ContractExecuteTransaction, + ContractFunctionParameters, +} from "../../src/exports.js"; +import IntegrationTestEnv from "./client/NodeIntegrationTestEnv.js"; + +describe("TransactionRecord", function () { + let env; + const contractByteCode = + "0x608060405234801561001057600080fd5b50610594806100206000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c8063368b87721461006757806353f2a8a31461009057806380acb9e6146100b1578063ce6d41de146100c4578063e21f37ce146100cc578063f7753390146100d4575b600080fd5b61007a610075366004610406565b6100dc565b60405161008791906104ba565b60405180910390f35b6100a361009e3660046104a2565b610112565b604051908152602001610087565b61007a6100bf366004610441565b610177565b61007a6101c2565b61007a610254565b61007a6102e2565b80516060906100f29060009060208501906102ef565b50506040805180820190915260028152614f4b60f01b6020820152919050565b6000606482116101735760405162461bcd60e51b815260206004820152602260248201527f546865206e756d62657220697320736d616c6c6572207468616e2068756e6472604482015261195960f21b606482015260840160405180910390fd5b5090565b815160609061018d9060009060208601906102ef565b5081516101a19060019060208501906102ef565b50506040805180820190915260028152614f4b60f01b602082015292915050565b6060600080546101d19061050d565b80601f01602080910402602001604051908101604052809291908181526020018280546101fd9061050d565b801561024a5780601f1061021f5761010080835404028352916020019161024a565b820191906000526020600020905b81548152906001019060200180831161022d57829003601f168201915b5050505050905090565b600080546102619061050d565b80601f016020809104026020016040519081016040528092919081815260200182805461028d9061050d565b80156102da5780601f106102af576101008083540402835291602001916102da565b820191906000526020600020905b8154815290600101906020018083116102bd57829003601f168201915b505050505081565b600180546102619061050d565b8280546102fb9061050d565b90600052602060002090601f01602090048101928261031d5760008555610363565b82601f1061033657805160ff1916838001178555610363565b82800160010185558215610363579182015b82811115610363578251825591602001919060010190610348565b506101739291505b80821115610173576000815560010161036b565b600082601f83011261038f578081fd5b813567ffffffffffffffff808211156103aa576103aa610548565b604051601f8301601f19908116603f011681019082821181831017156103d2576103d2610548565b816040528381528660208588010111156103ea578485fd5b8360208701602083013792830160200193909352509392505050565b600060208284031215610417578081fd5b813567ffffffffffffffff81111561042d578182fd5b6104398482850161037f565b949350505050565b60008060408385031215610453578081fd5b823567ffffffffffffffff8082111561046a578283fd5b6104768683870161037f565b9350602085013591508082111561048b578283fd5b506104988582860161037f565b9150509250929050565b6000602082840312156104b3578081fd5b5035919050565b6000602080835283518082850152825b818110156104e6578581018301518582016040015282016104ca565b818111156104f75783604083870101525b50601f01601f1916929092016040019392505050565b600181811c9082168061052157607f821691505b6020821081141561054257634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052604160045260246000fdfea26469706673582212206369861146f0c706a48e951145ffca721f1fe7e98b0fb535d02d2e0b4195be0664736f6c63430008040033"; + + before(async function () { + env = await IntegrationTestEnv.new({ throwaway: true }); + }); + + it("should return the verbose record log when a transaction failed ", async function () { + this.timeout(120000); + + const privateKey = env.operatorKey; + + // Create a file on Hedera and store the bytecode + const fileCreateTx = new FileCreateTransaction() + .setKeys([privateKey]) + .freezeWith(env.client); + const fileCreateSign = await fileCreateTx.sign(privateKey); + const fileCreateSubmit = await fileCreateSign.execute(env.client); + const fileCreateRx = await fileCreateSubmit.getReceipt(env.client); + const bytecodeFileId = fileCreateRx.fileId; + console.log(`- The bytecode file ID is: ${bytecodeFileId} \n`); + + //Append contents to the file + const fileAppendTx = new FileAppendTransaction() + .setFileId(bytecodeFileId) + .setContents(contractByteCode) + .setMaxChunks(10) + .freezeWith(env.client); + const fileAppendSign = await fileAppendTx.sign(privateKey); + const fileAppendSubmit = await fileAppendSign.execute(env.client); + const fileAppendRx = await fileAppendSubmit.getReceipt(env.client); + console.log("Status of file append is", fileAppendRx.status.toString()); + + // Instantiate the contract instance + const contractTx = await new ContractCreateTransaction() + //Set the file ID of the Hedera file storing the bytecode + .setBytecodeFileId(bytecodeFileId) + //Set the gas to instantiate the contract + .setGas(100000) + //Provide the constructor parameters for the contract + .setConstructorParameters(); + + //Submit the transaction to the Hedera test network + const contractResponse = await contractTx.execute(env.client); + + //Get the receipt of the file create transaction + const contractReceipt = await contractResponse.getReceipt(env.client); + + //Get the smart contract ID + const newContractId = contractReceipt.contractId; + + //Log the smart contract ID + console.log("The smart contract ID is " + newContractId); + + const contractExecuteTx = new ContractExecuteTransaction() + .setContractId(newContractId) + .setGas(750000) + .setFunction( + "setDataRequire", + new ContractFunctionParameters().addUint256(10) + ) + .freezeWith(env.client); + + try { + const signedTx = await contractExecuteTx.sign(privateKey); + const contractExecuteSubmit = await signedTx.execute(env.client); + + // Get transaction record information + await contractExecuteSubmit.getVerboseRecord(env.client); + } catch (err) { + expect(err.transactionRecord.transactionFee).not.to.be.null; + } + }); + + after(async function () { + await env.close(); + }); +});