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/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..ee837632c 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} from "./transaction-error" const TXID_REGEXP = /^[0-9a-fA-F]{64}$/ @@ -149,9 +150,17 @@ 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) { + const transactionError = new TransactionError.from(txStatus) + if (error != null) { + reject(transactionError) + unsub() + return + } else if (transactionError != null) { + reject(transactionError) + unsub() + return + } } else if (predicate(txStatus)) { resolve(txStatus) unsub() @@ -176,3 +185,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..59a436b5c --- /dev/null +++ b/packages/fcl-core/src/transaction/transaction-error.test.ts @@ -0,0 +1,61 @@ +import {FvmErrorCode, TransactionStatus} from "@onflow/typedefs" +import {TransactionError} from "./transaction-error" + +describe("TransactionError", () => { + test("parses transaction error from status", () => { + const status: TransactionStatus = { + blockId: "123", + status: 1, + statusString: "PENDING", + statusCode: 1, + errorMessage: "Transaction rejected by the network", + events: [], + } + const error = TransactionError.from(status) + expect(error).toBeInstanceOf(TransactionError) + expect(error!.message).toEqual("Transaction rejected by the network") + expect(error!.code).toBeUndefined() + }) + + test("parses transaction error with code from status", () => { + const status: TransactionStatus = { + blockId: "123", + status: 1, + statusString: "PENDING", + statusCode: 1, + errorMessage: "[Error Code: 1101] Some Cadence Error", + events: [], + } + const error = TransactionError.from(status) + expect(error).toBeInstanceOf(TransactionError) + expect(error!.message).toEqual("[Error Code: 1101] Some Cadence Error") + expect(error!.code).toEqual(FvmErrorCode.CADENCE_RUNTIME_ERROR) + }) + + test("returns null for successful transaction", () => { + const status: TransactionStatus = { + blockId: "123", + status: 1, + statusString: "PENDING", + statusCode: 0, + errorMessage: "", + events: [], + } + const error = TransactionError.from(status) + expect(error).toBeNull() + }) + + test("returns unknown error for missing error message", () => { + const status: TransactionStatus = { + blockId: "123", + status: 1, + statusString: "PENDING", + statusCode: 1, + errorMessage: "", + events: [], + } + const error = TransactionError.from(status) + expect(error).toBeInstanceOf(TransactionError) + expect(error!.message).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..231fb4fe8 --- /dev/null +++ b/packages/fcl-core/src/transaction/transaction-error.ts @@ -0,0 +1,21 @@ +import {FvmErrorCode, TransactionStatus} from "@onflow/typedefs" + +const ERROR_CODE_REGEX = /\[Error Code: (\d+)\]/ + +export class TransactionError extends Error { + public code?: FvmErrorCode + + private constructor(message: string, code?: FvmErrorCode) { + super(message) + this.code = code + } + + static from(status: TransactionStatus): TransactionError | null { + if (status.statusCode === 0) return null + + const match = status.errorMessage.match(ERROR_CODE_REGEX) + const code = match ? parseInt(match[1], 10) : undefined + + return new TransactionError(status.errorMessage || "Unknown error", code) + } +} diff --git a/packages/sdk/src/errors/errors.ts b/packages/sdk/src/errors/errors.ts new file mode 100644 index 000000000..776095ce7 --- /dev/null +++ b/packages/sdk/src/errors/errors.ts @@ -0,0 +1,32 @@ +import {TransactionStatus} from "@onflow/typedefs" + +export class TransactionError extends Error { + constructor( + public message: string, + public code?: T, + public name: string = "TransactionError" + ) { + super(message) + } + + static fromErrorMessage(message: string) { + return new TransactionError(message) + } + + static fromStatus( + status: TransactionStatus + ): TransactionError | null { + if (!status.errorMessage) return null + return TransactionError.fromErrorMessage(status.errorMessage) + } +} + +export class TransactionRejectedError extends TransactionError<403> { + constructor(message: string) { + super(message, 403, "TransactionRejectedError") + } + + static fromErrorMessage(message: string) { + return new TransactionRejectedError(message) + } +} diff --git a/packages/typedefs/src/fvm-errors.ts b/packages/typedefs/src/fvm-errors.ts new file mode 100644 index 000000000..1b063911c --- /dev/null +++ b/packages/typedefs/src/fvm-errors.ts @@ -0,0 +1,77 @@ +export enum FvmErrorCode { + // 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 097c3e477..c6d0250ba 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. */ @@ -346,6 +350,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, +} export type Provider = { /** * - Provider name. @@ -412,3 +427,4 @@ export type EventStream = StreamConnection<{ }> export * from "./interaction" +export * from "./fvm-errors"