From 432da5defde6f0c6b2c1c92870aa1ee0859148b4 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 21 Jul 2021 12:17:10 +0100 Subject: [PATCH] Add get transaction endpoint Signed-off-by: James Taylor --- .../rest-api-typescript/src/assets.router.ts | 31 +++-- .../rest-api-typescript/src/errors.ts | 12 ++ .../rest-api-typescript/src/fabric.ts | 39 ++++++- .../rest-api-typescript/src/server.ts | 9 +- .../src/transactions.router.ts | 108 ++++++++++++++++++ 5 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 asset-transfer-basic/rest-api-typescript/src/transactions.router.ts diff --git a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts index f70db1e3..ccf5690a 100644 --- a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts @@ -34,7 +34,7 @@ assetsRouter.get('/', async (req: Request, res: Response) => { logger.debug('Get all assets request received'); try { - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const data = await evatuateTransaction(contract, 'GetAllAssets'); const assets = JSON.parse(data.toString()); @@ -71,12 +71,12 @@ assetsRouter.post( }); } - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const redis: Redis = req.app.get('redis'); const assetId = req.body.id; try { - await submitTransaction( + const transactionId = await submitTransaction( contract, redis, 'CreateAsset', @@ -89,6 +89,7 @@ assetsRouter.post( return res.status(ACCEPTED).json({ status: getReasonPhrase(ACCEPTED), + transactionId: transactionId, timestamp: new Date().toISOString(), }); } catch (err) { @@ -121,7 +122,7 @@ assetsRouter.options('/:assetId', async (req: Request, res: Response) => { logger.debug('Asset options request received for asset ID %s', assetId); try { - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const data = await evatuateTransaction(contract, 'AssetExists', assetId); const exists = data.toString() === 'true'; @@ -160,7 +161,7 @@ assetsRouter.get('/:assetId', async (req: Request, res: Response) => { logger.debug('Read asset request received for asset ID %s', assetId); try { - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const data = await evatuateTransaction(contract, 'ReadAsset', assetId); const asset = JSON.parse(data.toString()); @@ -218,12 +219,12 @@ assetsRouter.put( }); } - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const redis: Redis = req.app.get('redis'); const assetId = req.params.assetId; try { - await submitTransaction( + const transactionId = await submitTransaction( contract, redis, 'UpdateAsset', @@ -236,6 +237,7 @@ assetsRouter.put( return res.status(ACCEPTED).json({ status: getReasonPhrase(ACCEPTED), + transactionId: transactionId, timestamp: new Date().toISOString(), }); } catch (err) { @@ -286,13 +288,13 @@ assetsRouter.patch( }); } - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const redis: Redis = req.app.get('redis'); const assetId = req.params.assetId; const newOwner = req.body[0].value; try { - await submitTransaction( + const transactionId = await submitTransaction( contract, redis, 'TransferAsset', @@ -302,6 +304,7 @@ assetsRouter.patch( return res.status(ACCEPTED).json({ status: getReasonPhrase(ACCEPTED), + transactionId: transactionId, timestamp: new Date().toISOString(), }); } catch (err) { @@ -330,15 +333,21 @@ assetsRouter.patch( assetsRouter.delete('/:assetId', async (req: Request, res: Response) => { logger.debug(req.body, 'Delete asset request received'); - const contract: Contract = req.app.get('contract'); + const contract: Contract = req.app.get('contracts').contract; const redis: Redis = req.app.get('redis'); const assetId = req.params.assetId; try { - await submitTransaction(contract, redis, 'DeleteAsset', assetId); + const transactionId = await submitTransaction( + contract, + redis, + 'DeleteAsset', + assetId + ); return res.status(ACCEPTED).json({ status: getReasonPhrase(ACCEPTED), + transactionId: transactionId, timestamp: new Date().toISOString(), }); } catch (err) { diff --git a/asset-transfer-basic/rest-api-typescript/src/errors.ts b/asset-transfer-basic/rest-api-typescript/src/errors.ts index beb03478..21692ac1 100644 --- a/asset-transfer-basic/rest-api-typescript/src/errors.ts +++ b/asset-transfer-basic/rest-api-typescript/src/errors.ts @@ -14,6 +14,18 @@ export class TransactionError extends Error { } } +export class TransactionNotFoundError extends Error { + transactionId: string; + + constructor(message: string, transactionId: string) { + super(message); + Object.setPrototypeOf(this, TransactionNotFoundError.prototype); + + this.name = 'TransactionNotFoundError'; + this.transactionId = transactionId; + } +} + export class AssetExistsError extends TransactionError { constructor(message: string, transactionId: string) { super(message, transactionId); diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.ts index cb4e551a..2419d0e1 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.ts @@ -21,9 +21,10 @@ import { AssetExistsError, AssetNotFoundError, TransactionError, + TransactionNotFoundError, } from './errors'; -export const getContract = async (): Promise => { +export const getGateway = async (): Promise => { const wallet = await Wallets.newInMemoryWallet(); const x509Identity = { @@ -55,10 +56,18 @@ export const getContract = async (): Promise => { await gateway.connect(config.connectionProfile, connectOptions); - const network = await gateway.getNetwork(config.channelName); - const contract = network.getContract(config.chaincodeName); + return gateway; +}; - return contract; +export const getContracts = async ( + gateway: Gateway +): Promise<{ contract: Contract; qscc: Contract }> => { + const network = await gateway.getNetwork(config.channelName); + + const contract = network.getContract(config.chaincodeName); + const qscc = network.getContract('qscc'); + + return { contract, qscc }; }; export const createDeferredEventHandler = ( @@ -257,6 +266,28 @@ const handleError = (transactionId: string, err: Error): Error => { 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/server.ts b/asset-transfer-basic/rest-api-typescript/src/server.ts index f22f5fdd..180278c4 100644 --- a/asset-transfer-basic/rest-api-typescript/src/server.ts +++ b/asset-transfer-basic/rest-api-typescript/src/server.ts @@ -9,7 +9,8 @@ import pinoMiddleware from 'pino-http'; import { logger } from './logger'; import { assetsRouter } from './assets.router'; -import { getContract } from './fabric'; +import { transactionsRouter } from './transactions.router'; +import { getContracts, getGateway } from './fabric'; import { redis } from './redis'; const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes; @@ -48,8 +49,9 @@ export const createServer = async (): Promise => { app.use(helmet()); } - const contract = await getContract(); - app.set('contract', contract); + const gateway = await getGateway(); + const contracts = await getContracts(gateway); + app.set('contracts', contracts); app.set('redis', redis); // Health routes @@ -72,6 +74,7 @@ export const createServer = async (): Promise => { }); app.use('/api/assets', assetsRouter); + app.use('/api/transactions', transactionsRouter); // For everything else app.use((_req, res) => diff --git a/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts b/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts new file mode 100644 index 00000000..2a0bc987 --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/src/transactions.router.ts @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import express, { Request, Response } from 'express'; +import { Contract } from 'fabric-network'; +import { protos } from 'fabric-protos'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; +import { Redis } from 'ioredis'; +import { evatuateTransaction } from './fabric'; +import { logger } from './logger'; +import * as config from './config'; +import { TransactionNotFoundError } from './errors'; + +const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes; + +export const transactionsRouter = express.Router(); + +type Progress = 'ACCEPTED' | 'RETRYING' | 'DONE'; + +transactionsRouter.get( + '/:transactionId', + async (req: Request, res: Response) => { + const transactionId = req.params.transactionId; + logger.debug('Read request received for transaction ID %s', transactionId); + + let foundTransaction = false; + let progress: Progress = 'DONE'; + let validationCode = ''; + + const qscc: Contract = req.app.get('contracts').qscc; + const redis: Redis = req.app.get('redis'); + + try { + const savedTransaction = await (redis as Redis).hgetall( + `txn:${transactionId}` + ); + logger.debug( + { transactionId: transactionId, state: savedTransaction }, + 'Saved transaction state' + ); + + if (savedTransaction.state) { + foundTransaction = true; + const retries = parseInt(savedTransaction.retries); + if (retries > 0) { + progress = 'RETRYING'; + } else { + progress = 'ACCEPTED'; + } + } + } catch (err) { + logger.error( + err, + 'Redis error processing read request for transaction ID %s', + transactionId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } + + try { + const data = await evatuateTransaction( + qscc, + 'GetTransactionByID', + config.channelName, + transactionId + ); + + foundTransaction = true; + // TODO is it possible to use the BlockDecoder decodeTransaction + // function in fabric-common? + const processedTransaction = protos.ProcessedTransaction.decode(data); + validationCode = + protos.TxValidationCode[processedTransaction.validationCode]; + } catch (err) { + if (!(err instanceof TransactionNotFoundError)) { + logger.error( + err, + 'Fabric error processing read request for transaction ID %s', + transactionId + ); + + return res.status(INTERNAL_SERVER_ERROR).json({ + status: getReasonPhrase(INTERNAL_SERVER_ERROR), + timestamp: new Date().toISOString(), + }); + } + } + + if (foundTransaction) { + return res.status(OK).json({ + status: getReasonPhrase(OK), + progress: progress, + validationCode: validationCode, + timestamp: new Date().toISOString(), + }); + } else { + return res.status(NOT_FOUND).json({ + status: getReasonPhrase(NOT_FOUND), + timestamp: new Date().toISOString(), + }); + } + } +);