diff --git a/.changeset/pink-students-divide.md b/.changeset/pink-students-divide.md new file mode 100644 index 000000000..4f0849df4 --- /dev/null +++ b/.changeset/pink-students-divide.md @@ -0,0 +1,5 @@ +--- +"@onflow/typedefs": minor +--- + +Add missing field to TransactionStatus type diff --git a/.changeset/pre.json b/.changeset/pre.json index 85f8e9194..1577f7398 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -25,8 +25,5 @@ "@onflow/util-template": "1.2.2", "@onflow/util-uid": "1.2.2" }, - "changesets": [ - "slow-lies-fix", - "tasty-ducks-mix" - ] + "changesets": ["slow-lies-fix", "tasty-ducks-mix"] } diff --git a/.changeset/soft-tomatoes-brake.md b/.changeset/soft-tomatoes-brake.md new file mode 100644 index 000000000..719088952 --- /dev/null +++ b/.changeset/soft-tomatoes-brake.md @@ -0,0 +1,5 @@ +--- +"@onflow/typedefs": minor +--- + +Add FvmErrorCode enum for categorizing transaction/script execution errors diff --git a/.changeset/witty-pants-argue.md b/.changeset/witty-pants-argue.md new file mode 100644 index 000000000..2e52dd184 --- /dev/null +++ b/.changeset/witty-pants-argue.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl-core": minor +--- + +Add custom error `TransactionError` type for failing transaction results diff --git a/packages/fcl-core/src/transaction/index.js b/packages/fcl-core/src/transaction/index.js index 3f985269a..bf7eab0d7 100644 --- a/packages/fcl-core/src/transaction/index.js +++ b/packages/fcl-core/src/transaction/index.js @@ -12,6 +12,7 @@ import { import {send as fclSend, decode, getTransactionStatus} from "@onflow/sdk" import {HTTPRequestError} from "@onflow/transport-http" import {grpc} from "@improbable-eng/grpc-web" +import {TransactionError, parseTransactionErrorCode} from "./transaction-error" const TXID_REGEXP = /^[0-9a-fA-F]{64}$/ @@ -149,9 +150,19 @@ export function transaction( const suppress = opts.suppress || false return new Promise((resolve, reject) => { const unsub = subscribe((txStatus, error) => { - if ((error || txStatus.statusCode) && !suppress) { - reject(error || txStatus.errorMessage) - unsub() + if (!suppress) { + if (error != null) { + reject(error) + unsub() + return + } else if (txStatus.statusCode === 1) { + const transactionError = TransactionError.fromErrorMessage( + txStatus.errorMessage + ) + reject(transactionError) + unsub() + return + } } else if (predicate(txStatus)) { resolve(txStatus) unsub() @@ -176,3 +187,5 @@ transaction.isFinalized = isFinalized transaction.isExecuted = isExecuted transaction.isSealed = isSealed transaction.isExpired = isExpired + +export {TransactionError} diff --git a/packages/fcl-core/src/transaction/transaction-error.test.ts b/packages/fcl-core/src/transaction/transaction-error.test.ts new file mode 100644 index 000000000..3506787f7 --- /dev/null +++ b/packages/fcl-core/src/transaction/transaction-error.test.ts @@ -0,0 +1,48 @@ +import {FvmErrorCode} from "@onflow/typedefs" +import {TransactionError} from "./transaction-error" + +describe("TransactionError", () => { + describe("fromErrorMessage", () => { + test("returns unknown error if no code exists", () => { + const errorMessage = "Transaction rejected by the network" + const error = TransactionError.fromErrorMessage(errorMessage) + expect(error).toBeInstanceOf(TransactionError) + expect(error.code).toEqual(FvmErrorCode.UNKNOWN_ERROR) + expect(error.type).toEqual("UNKNOWN_ERROR") + }) + + test("parses transaction error with code from status", () => { + const errorMessage = "[Error Code: 1101] Some Cadence Error" + const error = TransactionError.fromErrorMessage(errorMessage) + expect(error).toBeInstanceOf(TransactionError) + expect(error.code).toEqual(FvmErrorCode.CADENCE_RUNTIME_ERROR) + expect(error.type).toEqual("CADENCE_RUNTIME_ERROR") + }) + + test("uses first instance of error code in message", () => { + const errorMessage = + "[Error Code: 1102] Unsupported value... [Error Code: 1105] Something else to say" + const error = TransactionError.fromErrorMessage(errorMessage) + expect(error).toBeInstanceOf(TransactionError) + expect(error.code).toEqual(FvmErrorCode.ENCODING_UNSUPPORTED_VALUE) + expect(error.type).toEqual("ENCODING_UNSUPPORTED_VALUE") + }) + + test("allows leading text before error code", () => { + const errorMessage = + "This is a message [Error Code: 1102] Unsupported value" + const error = TransactionError.fromErrorMessage(errorMessage) + expect(error).toBeInstanceOf(TransactionError) + expect(error.code).toEqual(FvmErrorCode.ENCODING_UNSUPPORTED_VALUE) + expect(error.type).toEqual("ENCODING_UNSUPPORTED_VALUE") + }) + + test("returns unknown error for missing error message", () => { + const errorMessage = "" + const error = TransactionError.fromErrorMessage(errorMessage) + expect(error).toBeInstanceOf(TransactionError) + expect(error.code).toEqual(FvmErrorCode.UNKNOWN_ERROR) + expect(error.type).toEqual("UNKNOWN_ERROR") + }) + }) +}) diff --git a/packages/fcl-core/src/transaction/transaction-error.ts b/packages/fcl-core/src/transaction/transaction-error.ts new file mode 100644 index 000000000..4fca209ab --- /dev/null +++ b/packages/fcl-core/src/transaction/transaction-error.ts @@ -0,0 +1,24 @@ +import {FvmErrorCode} from "@onflow/typedefs" + +const ERROR_CODE_REGEX = /\[Error Code: (\d+)\]/ + +export class TransactionError extends Error { + public code: FvmErrorCode + public type: string + + private constructor(message: string, code: FvmErrorCode) { + super(message) + this.code = code + this.type = FvmErrorCode[code] + } + + static fromErrorMessage(errorMessage: string): TransactionError { + const match = errorMessage.match(ERROR_CODE_REGEX) + const code = match ? parseInt(match[1], 10) : undefined + + return new TransactionError( + errorMessage, + code || FvmErrorCode.UNKNOWN_ERROR + ) + } +} diff --git a/packages/typedefs/src/fvm-errors.ts b/packages/typedefs/src/fvm-errors.ts new file mode 100644 index 000000000..9c5d68441 --- /dev/null +++ b/packages/typedefs/src/fvm-errors.ts @@ -0,0 +1,80 @@ +export enum FvmErrorCode { + // We use -1 for unknown error in FCL because FVM defines error codes as uint16 + // This means we have no risk of collision with FVM error codes + UNKNOWN_ERROR = -1, + // tx validation errors 1000 - 1049 + // Deprecated: no longer in use + TX_VALIDATION_ERROR = 1000, + // Deprecated: No longer used. + INVALID_TX_BYTE_SIZE_ERROR = 1001, + // Deprecated: No longer used. + INVALID_REFERENCE_BLOCK_ERROR = 1002, + // Deprecated: No longer used. + EXPIRED_TRANSACTION_ERROR = 1003, + // Deprecated: No longer used. + INVALID_SCRIPT_ERROR = 1004, + // Deprecated: No longer used. + INVALID_GAS_LIMIT_ERROR = 1005, + INVALID_PROPOSAL_SIGNATURE_ERROR = 1006, + INVALID_PROPOSAL_SEQ_NUMBER_ERROR = 1007, + INVALID_PAYLOAD_SIGNATURE_ERROR = 1008, + INVALID_ENVELOPE_SIGNATURE_ERROR = 1009, + + // base errors 1050 - 1100 + // Deprecated: No longer used. + FVM_INTERNAL_ERROR = 1050, + VALUE_ERROR = 1051, + INVALID_ARGUMENT_ERROR = 1052, + INVALID_ADDRESS_ERROR = 1053, + INVALID_LOCATION_ERROR = 1054, + ACCOUNT_AUTHORIZATION_ERROR = 1055, + OPERATION_AUTHORIZATION_ERROR = 1056, + OPERATION_NOT_SUPPORTED_ERROR = 1057, + BLOCK_HEIGHT_OUT_OF_RANGE_ERROR = 1058, + + // execution errors 1100 - 1200 + // Deprecated: No longer used. + EXECUTION_ERROR = 1100, + CADENCE_RUNTIME_ERROR = 1101, + // Deprecated: No longer used. + ENCODING_UNSUPPORTED_VALUE = 1102, + STORAGE_CAPACITY_EXCEEDED = 1103, + // Deprecated: No longer used. + GAS_LIMIT_EXCEEDED_ERROR = 1104, + EVENT_LIMIT_EXCEEDED_ERROR = 1105, + LEDGER_INTERACTION_LIMIT_EXCEEDED_ERROR = 1106, + STATE_KEY_SIZE_LIMIT_ERROR = 1107, + STATE_VALUE_SIZE_LIMIT_ERROR = 1108, + TRANSACTION_FEE_DEDUCTION_FAILED_ERROR = 1109, + COMPUTATION_LIMIT_EXCEEDED_ERROR = 1110, + MEMORY_LIMIT_EXCEEDED_ERROR = 1111, + COULD_NOT_DECODE_EXECUTION_PARAMETER_FROM_STATE = 1112, + SCRIPT_EXECUTION_TIMED_OUT_ERROR = 1113, + SCRIPT_EXECUTION_CANCELLED_ERROR = 1114, + EVENT_ENCODING_ERROR = 1115, + INVALID_INTERNAL_STATE_ACCESS_ERROR = 1116, + // 1117 was never deployed and is free to use + INSUFFICIENT_PAYER_BALANCE = 1118, + + // accounts errors 1200 - 1250 + // Deprecated: No longer used. + ACCOUNT_ERROR = 1200, + ACCOUNT_NOT_FOUND_ERROR = 1201, + ACCOUNT_PUBLIC_KEY_NOT_FOUND_ERROR = 1202, + ACCOUNT_ALREADY_EXISTS_ERROR = 1203, + // Deprecated: No longer used. + FROZEN_ACCOUNT_ERROR = 1204, + // Deprecated: No longer used. + ACCOUNT_STORAGE_NOT_INITIALIZED_ERROR = 1205, + ACCOUNT_PUBLIC_KEY_LIMIT_ERROR = 1206, + + // contract errors 1250 - 1300 + // Deprecated: No longer used. + CONTRACT_ERROR = 1250, + CONTRACT_NOT_FOUND_ERROR = 1251, + // Deprecated: No longer used. + CONTRACT_NAMES_NOT_FOUND_ERROR = 1252, + + // fvm std lib errors 1300-1400 + EVM_EXECUTION_ERROR = 1300, +} diff --git a/packages/typedefs/src/index.ts b/packages/typedefs/src/index.ts index c5b8b318d..4ff8735a6 100644 --- a/packages/typedefs/src/index.ts +++ b/packages/typedefs/src/index.ts @@ -330,13 +330,17 @@ export type TransactionStatus = { */ blockId: string /** - * - The status code of the transaction. + * - The execution status of the transaction */ - status: number + status: TransactionExecutionStatus /** * - The status as as descriptive text (e.g. "FINALIZED"). */ statusString: string + /** + * - The result of the transaction, if executed (i.e. 0 for success, 1 for failure) + */ + statusCode: 0 | 1 /** * - The error message of the transaction. */ @@ -347,6 +351,17 @@ export type TransactionStatus = { events: Array } /** + * The execution status of the transaction. + */ +export enum TransactionExecutionStatus { + UNKNOWN = 0, + PENDING = 1, + FINALIZED = 2, + EXECUTED = 3, + SEALED = 4, + EXPIRED = 5, +} +/* * The Provider type describes a Wallet Provider associated with a specific Service. */ export type Provider = { @@ -443,3 +458,4 @@ export type EventStream = StreamConnection<{ }> export * from "./interaction" +export * from "./fvm-errors"