Add get transaction endpoint

Signed-off-by: James Taylor <jamest@uk.ibm.com>
This commit is contained in:
James Taylor 2021-07-21 12:17:10 +01:00
parent 60aedf1b82
commit 432da5defd
5 changed files with 181 additions and 18 deletions

View file

@ -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) {

View file

@ -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);

View file

@ -21,9 +21,10 @@ import {
AssetExistsError,
AssetNotFoundError,
TransactionError,
TransactionNotFoundError,
} from './errors';
export const getContract = async (): Promise<Contract> => {
export const getGateway = async (): Promise<Gateway> => {
const wallet = await Wallets.newInMemoryWallet();
const x509Identity = {
@ -55,10 +56,18 @@ export const getContract = async (): Promise<Contract> => {
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);
};

View file

@ -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<Application> => {
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<Application> => {
});
app.use('/api/assets', assetsRouter);
app.use('/api/transactions', transactionsRouter);
// For everything else
app.use((_req, res) =>

View file

@ -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(),
});
}
}
);