diff --git a/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts b/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts new file mode 100644 index 00000000..a0813c90 --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/src/errors.spec.ts @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + AssetExistsError, + AssetNotFoundError, + TransactionError, + TransactionNotFoundError, + handleError, + isDuplicateTransactionError, +} from './errors'; + +describe('Errors', () => { + describe('isDuplicateTransactionError', () => { + it('returns true for an error with duplicate transaction endorsement details', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'duplicate transaction found', + }, + ], + }, + ], + }; + + expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( + true + ); + }); + + it('returns false for an error without duplicate transaction endorsement details', () => { + const mockDuplicateTransactionError = { + errors: [ + { + endorsements: [ + { + details: 'mock endorsement details', + }, + ], + }, + ], + }; + + expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe( + false + ); + }); + }); + + describe('handleError', () => { + it.each([ + 'the asset GOCHAINCODE already exists', + 'Asset JAVACHAINCODE already exists', + 'The asset JSCHAINCODE already exists', + ])( + 'returns an AssetExistsError for errors with an asset already exists message: %s', + (msg) => { + expect(handleError('txn1', new Error(msg))).toStrictEqual( + new AssetExistsError(msg, 'txn1') + ); + } + ); + + it.each([ + 'the asset GOCHAINCODE does not exist', + 'Asset JAVACHAINCODE does not exist', + 'The asset JSCHAINCODE does not exist', + ])( + 'returns an AssetNotFoundError for errors with an asset does not exist message: %s', + (msg) => { + expect(handleError('txn1', new Error(msg))).toStrictEqual( + new AssetNotFoundError(msg, 'txn1') + ); + } + ); + + it('returns a TransactionNotFoundError for errors with a transaction not found message', () => { + expect( + handleError( + 'txn1', + new Error( + 'Failed to get transaction with id txn, error Entry not found in index' + ) + ) + ).toStrictEqual( + new TransactionNotFoundError( + 'Failed to get transaction with id txn, error Entry not found in index', + 'txn1' + ) + ); + }); + + it('returns a TransactionError for errors with other messages', () => { + expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual( + new TransactionError('Transaction error', 'txn1') + ); + }); + }); +}); diff --git a/asset-transfer-basic/rest-api-typescript/src/errors.ts b/asset-transfer-basic/rest-api-typescript/src/errors.ts index 21692ac1..5a1fde2e 100644 --- a/asset-transfer-basic/rest-api-typescript/src/errors.ts +++ b/asset-transfer-basic/rest-api-typescript/src/errors.ts @@ -2,6 +2,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { logger } from './logger'; + export class TransactionError extends Error { transactionId: string; @@ -43,3 +45,91 @@ export class AssetNotFoundError extends TransactionError { this.name = 'AssetNotFoundError'; } } + +/* + * Checks whether an error was caused by a duplicate transaction. + * + * Checking error strings like this is not ideal, unfortunately it appears to + * be the only option. In this case it would be better to check for the + * DUPLICATE_TXID TxValidationCode somehow but that does not seem to be + * possible. + */ +export const isDuplicateTransactionError = (error: { + errors: { endorsements: { details: string }[] }[]; +}): boolean => { + try { + const isDuplicateTxn = error?.errors?.some((err) => + err?.endorsements?.some((endorsement) => + endorsement?.details?.startsWith('duplicate transaction found') + ) + ); + + return isDuplicateTxn; + } catch (err) { + logger.warn(err, 'Error checking for duplicate transaction'); + } + + return false; +}; + +/* + * Handles errors from evaluating and submitting transactions. + * + * As with duplicate transaction errors, checking error strings like this is + * not ideal. Unfortunately the chaincode samples do not use error codes so + * again it's the only option. The error message text is not even the same for + * the Go, Java, and Javascript implementations of the chaincode! + */ +export const handleError = (transactionId: string, err: Error): Error => { + // This regex needs to match the following error messages: + // "the asset %s already exists" + // "The asset ${id} already exists" + // "Asset %s already exists" + const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g; + const assetAlreadyExistsMatch = err.message.match(assetAlreadyExistsRegex); + logger.debug( + { message: err.message, result: assetAlreadyExistsMatch }, + 'Checking for asset already exists message' + ); + if (assetAlreadyExistsMatch) { + return new AssetExistsError(assetAlreadyExistsMatch[0], transactionId); + } + + // This regex needs to match the following error messages: + // "the asset %s does not exist" + // "The asset ${id} does not exist" + // "Asset %s does not exist" + const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g; + const assetDoesNotExistMatch = err.message.match(assetDoesNotExistRegex); + logger.debug( + { message: err.message, result: assetDoesNotExistMatch }, + 'Checking for asset does not exist message' + ); + if (assetDoesNotExistMatch) { + return new AssetNotFoundError(assetDoesNotExistMatch[0], transactionId); + } + + // This regex needs to match the following error messages: + // "Failed to get transaction with id %s, error Entry not found in index" + const transactionDoesNotExistRegex = + /Failed to get transaction with id [^,]*, error Entry not found in index/g; + const transactionDoesNotExistMatch = err.message.match( + transactionDoesNotExistRegex + ); + logger.debug( + { message: err.message, result: transactionDoesNotExistMatch }, + 'Checking for transaction does not exist message' + ); + if (transactionDoesNotExistMatch) { + return new TransactionNotFoundError( + transactionDoesNotExistMatch[0], + transactionId + ); + } + + logger.error( + { transactionId: transactionId, error: err }, + 'Unhandled transaction error' + ); + return new TransactionError('Transaction error', transactionId); +}; diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts index f871c395..a551dac9 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts @@ -320,35 +320,25 @@ describe('Fabric', () => { expect(result.toString()).toBe(mockPayload.toString()); }); - it.each([ - 'the asset GOCHAINCODE already exists', - 'Asset JAVACHAINCODE already exists', - 'The asset JSCHAINCODE already exists', - ])( - 'throws an AssetExistsError an asset already exists error occurs: %s', - async (msg) => { - mockTransaction.evaluate.mockRejectedValue(new Error(msg)); + it('throws an AssetExistsError an asset already exists error occurs', async () => { + mockTransaction.evaluate.mockRejectedValue( + new Error('The asset JSCHAINCODE already exists') + ); - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(AssetExistsError); - } - ); + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(AssetExistsError); + }); - it.each([ - 'the asset GOCHAINCODE does not exist', - 'Asset JAVACHAINCODE does not exist', - 'The asset JSCHAINCODE does not exist', - ])( - 'throws an AssetNotFoundError if an asset does not exist error occurs: %s', - async (msg) => { - mockTransaction.evaluate.mockRejectedValue(new Error(msg)); + it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { + mockTransaction.evaluate.mockRejectedValue( + new Error('The asset JSCHAINCODE does not exist') + ); - await expect(async () => { - await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); - }).rejects.toThrow(AssetNotFoundError); - } - ); + await expect(async () => { + await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); + }).rejects.toThrow(AssetNotFoundError); + }); it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { mockTransaction.evaluate.mockRejectedValue( diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.ts index acb81d5c..aa8586ea 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.ts @@ -25,12 +25,7 @@ import { incrementRetryCount, TransactionDetails, } from './redis'; -import { - AssetExistsError, - AssetNotFoundError, - TransactionError, - TransactionNotFoundError, -} from './errors'; +import { handleError, isDuplicateTransactionError } from './errors'; import protos from 'fabric-protos'; /* @@ -247,63 +242,6 @@ export const submitTransaction = async ( return txnId; }; -// Unfortunately the chaincode samples do not use error codes, and the error -// message text is not the same for each implementation -// TODO move to errors.ts? -const handleError = (transactionId: string, err: Error): Error => { - // This regex needs to match the following error messages: - // "the asset %s already exists" - // "The asset ${id} already exists" - // "Asset %s already exists" - const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g; - const assetAlreadyExistsMatch = err.message.match(assetAlreadyExistsRegex); - logger.debug( - { message: err.message, result: assetAlreadyExistsMatch }, - 'Checking for asset already exists message' - ); - if (assetAlreadyExistsMatch) { - return new AssetExistsError(assetAlreadyExistsMatch[0], transactionId); - } - - // This regex needs to match the following error messages: - // "the asset %s does not exist" - // "The asset ${id} does not exist" - // "Asset %s does not exist" - const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g; - const assetDoesNotExistMatch = err.message.match(assetDoesNotExistRegex); - logger.debug( - { message: err.message, result: assetDoesNotExistMatch }, - 'Checking for asset does not exist message' - ); - if (assetDoesNotExistMatch) { - return new AssetNotFoundError(assetDoesNotExistMatch[0], transactionId); - } - - // This regex needs to match the following error messages: - // "Failed to get transaction with id %s, error Entry not found in index" - const transactionDoesNotExistRegex = - /Failed to get transaction with id [^,]*, error Entry not found in index/g; - const transactionDoesNotExistMatch = err.message.match( - transactionDoesNotExistRegex - ); - logger.debug( - { message: err.message, result: transactionDoesNotExistMatch }, - 'Checking for transaction does not exist message' - ); - if (transactionDoesNotExistMatch) { - return new TransactionNotFoundError( - transactionDoesNotExistMatch[0], - transactionId - ); - } - - logger.error( - { transactionId: transactionId, error: err }, - 'Unhandled transaction error' - ); - return new TransactionError('Transaction error', transactionId); -}; - /* * Retry a transaction * @@ -357,26 +295,6 @@ const retryTransaction = async ( } }; -// TODO move to errors.ts? -const isDuplicateTransactionError = (error: { - errors: { endorsements: { details: string }[] }[]; -}) => { - // TODO this is horrible! Isn't it possible to check for TxValidationCode DUPLICATE_TXID somehow? - try { - const isDuplicateTxn = error?.errors?.some((err) => - err?.endorsements?.some((endorsement) => - endorsement?.details?.startsWith('duplicate transaction found') - ) - ); - - return isDuplicateTxn; - } catch (err) { - logger.warn(err, 'Error checking for duplicate transaction'); - } - - return false; -}; - /* * Block event listener to handle successful transactions * @@ -409,9 +327,9 @@ export const blockEventHandler = (redis: Redis): BlockListener => { /* * Get the current block height - * + * * This example of using a system contract is used for the liveness REST - * endpoint + * endpoint */ export const getBlockHeight = async ( qscc: Contract