mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
Add get transaction endpoint
Signed-off-by: James Taylor <jamest@uk.ibm.com>
This commit is contained in:
parent
60aedf1b82
commit
432da5defd
5 changed files with 181 additions and 18 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
Loading…
Reference in a new issue