mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-22 17:45:10 +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');
|
logger.debug('Get all assets request received');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contract: Contract = req.app.get('contract');
|
const contract: Contract = req.app.get('contracts').contract;
|
||||||
|
|
||||||
const data = await evatuateTransaction(contract, 'GetAllAssets');
|
const data = await evatuateTransaction(contract, 'GetAllAssets');
|
||||||
const assets = JSON.parse(data.toString());
|
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 redis: Redis = req.app.get('redis');
|
||||||
const assetId = req.body.id;
|
const assetId = req.body.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submitTransaction(
|
const transactionId = await submitTransaction(
|
||||||
contract,
|
contract,
|
||||||
redis,
|
redis,
|
||||||
'CreateAsset',
|
'CreateAsset',
|
||||||
|
|
@ -89,6 +89,7 @@ assetsRouter.post(
|
||||||
|
|
||||||
return res.status(ACCEPTED).json({
|
return res.status(ACCEPTED).json({
|
||||||
status: getReasonPhrase(ACCEPTED),
|
status: getReasonPhrase(ACCEPTED),
|
||||||
|
transactionId: transactionId,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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);
|
logger.debug('Asset options request received for asset ID %s', assetId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contract: Contract = req.app.get('contract');
|
const contract: Contract = req.app.get('contracts').contract;
|
||||||
|
|
||||||
const data = await evatuateTransaction(contract, 'AssetExists', assetId);
|
const data = await evatuateTransaction(contract, 'AssetExists', assetId);
|
||||||
const exists = data.toString() === 'true';
|
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);
|
logger.debug('Read asset request received for asset ID %s', assetId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contract: Contract = req.app.get('contract');
|
const contract: Contract = req.app.get('contracts').contract;
|
||||||
|
|
||||||
const data = await evatuateTransaction(contract, 'ReadAsset', assetId);
|
const data = await evatuateTransaction(contract, 'ReadAsset', assetId);
|
||||||
const asset = JSON.parse(data.toString());
|
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 redis: Redis = req.app.get('redis');
|
||||||
const assetId = req.params.assetId;
|
const assetId = req.params.assetId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submitTransaction(
|
const transactionId = await submitTransaction(
|
||||||
contract,
|
contract,
|
||||||
redis,
|
redis,
|
||||||
'UpdateAsset',
|
'UpdateAsset',
|
||||||
|
|
@ -236,6 +237,7 @@ assetsRouter.put(
|
||||||
|
|
||||||
return res.status(ACCEPTED).json({
|
return res.status(ACCEPTED).json({
|
||||||
status: getReasonPhrase(ACCEPTED),
|
status: getReasonPhrase(ACCEPTED),
|
||||||
|
transactionId: transactionId,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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 redis: Redis = req.app.get('redis');
|
||||||
const assetId = req.params.assetId;
|
const assetId = req.params.assetId;
|
||||||
const newOwner = req.body[0].value;
|
const newOwner = req.body[0].value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submitTransaction(
|
const transactionId = await submitTransaction(
|
||||||
contract,
|
contract,
|
||||||
redis,
|
redis,
|
||||||
'TransferAsset',
|
'TransferAsset',
|
||||||
|
|
@ -302,6 +304,7 @@ assetsRouter.patch(
|
||||||
|
|
||||||
return res.status(ACCEPTED).json({
|
return res.status(ACCEPTED).json({
|
||||||
status: getReasonPhrase(ACCEPTED),
|
status: getReasonPhrase(ACCEPTED),
|
||||||
|
transactionId: transactionId,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -330,15 +333,21 @@ assetsRouter.patch(
|
||||||
assetsRouter.delete('/:assetId', async (req: Request, res: Response) => {
|
assetsRouter.delete('/:assetId', async (req: Request, res: Response) => {
|
||||||
logger.debug(req.body, 'Delete asset request received');
|
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 redis: Redis = req.app.get('redis');
|
||||||
const assetId = req.params.assetId;
|
const assetId = req.params.assetId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submitTransaction(contract, redis, 'DeleteAsset', assetId);
|
const transactionId = await submitTransaction(
|
||||||
|
contract,
|
||||||
|
redis,
|
||||||
|
'DeleteAsset',
|
||||||
|
assetId
|
||||||
|
);
|
||||||
|
|
||||||
return res.status(ACCEPTED).json({
|
return res.status(ACCEPTED).json({
|
||||||
status: getReasonPhrase(ACCEPTED),
|
status: getReasonPhrase(ACCEPTED),
|
||||||
|
transactionId: transactionId,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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 {
|
export class AssetExistsError extends TransactionError {
|
||||||
constructor(message: string, transactionId: string) {
|
constructor(message: string, transactionId: string) {
|
||||||
super(message, transactionId);
|
super(message, transactionId);
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ import {
|
||||||
AssetExistsError,
|
AssetExistsError,
|
||||||
AssetNotFoundError,
|
AssetNotFoundError,
|
||||||
TransactionError,
|
TransactionError,
|
||||||
|
TransactionNotFoundError,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
|
|
||||||
export const getContract = async (): Promise<Contract> => {
|
export const getGateway = async (): Promise<Gateway> => {
|
||||||
const wallet = await Wallets.newInMemoryWallet();
|
const wallet = await Wallets.newInMemoryWallet();
|
||||||
|
|
||||||
const x509Identity = {
|
const x509Identity = {
|
||||||
|
|
@ -55,10 +56,18 @@ export const getContract = async (): Promise<Contract> => {
|
||||||
|
|
||||||
await gateway.connect(config.connectionProfile, connectOptions);
|
await gateway.connect(config.connectionProfile, connectOptions);
|
||||||
|
|
||||||
const network = await gateway.getNetwork(config.channelName);
|
return gateway;
|
||||||
const contract = network.getContract(config.chaincodeName);
|
};
|
||||||
|
|
||||||
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 = (
|
export const createDeferredEventHandler = (
|
||||||
|
|
@ -257,6 +266,28 @@ const handleError = (transactionId: string, err: Error): Error => {
|
||||||
return new AssetNotFoundError(assetDoesNotExistMatch[0], transactionId);
|
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);
|
return new TransactionError('Transaction error', transactionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import pinoMiddleware from 'pino-http';
|
||||||
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { assetsRouter } from './assets.router';
|
import { assetsRouter } from './assets.router';
|
||||||
import { getContract } from './fabric';
|
import { transactionsRouter } from './transactions.router';
|
||||||
|
import { getContracts, getGateway } from './fabric';
|
||||||
import { redis } from './redis';
|
import { redis } from './redis';
|
||||||
|
|
||||||
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
|
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
|
||||||
|
|
@ -48,8 +49,9 @@ export const createServer = async (): Promise<Application> => {
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
}
|
}
|
||||||
|
|
||||||
const contract = await getContract();
|
const gateway = await getGateway();
|
||||||
app.set('contract', contract);
|
const contracts = await getContracts(gateway);
|
||||||
|
app.set('contracts', contracts);
|
||||||
app.set('redis', redis);
|
app.set('redis', redis);
|
||||||
|
|
||||||
// Health routes
|
// Health routes
|
||||||
|
|
@ -72,6 +74,7 @@ export const createServer = async (): Promise<Application> => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api/assets', assetsRouter);
|
app.use('/api/assets', assetsRouter);
|
||||||
|
app.use('/api/transactions', transactionsRouter);
|
||||||
|
|
||||||
// For everything else
|
// For everything else
|
||||||
app.use((_req, res) =>
|
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