Initial create asset logic

Signed-off-by: James Taylor <jamest@uk.ibm.com>
This commit is contained in:
James Taylor 2021-07-02 12:13:15 +01:00
parent 324f1c8683
commit 3b50404763
10 changed files with 542 additions and 20 deletions

View file

@ -1,2 +1,23 @@
LOG_LEVEL=info
PORT=3000
RETRY_DELAY=3000
HLF_CONNECTION_PROFILE={"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }
HLF_CERTIFICATE="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
HLF_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
HLF_COMMIT_TIMEOUT=3000
HLF_ENDORSE_TIMEOUT=30
REDIS_HOST=localhost
REDIS_PORT=6379
#REDIS_USERNAME=
#REDIS_PASSWORD=

View file

@ -237,6 +237,15 @@
"@types/range-parser": "*"
}
},
"@types/ioredis": {
"version": "4.26.4",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.26.4.tgz",
"integrity": "sha512-QFbjNq7EnOGw6d1gZZt2h26OFXjx7z+eqEnbCHSrDI1OOLEgOHMKdtIajJbuCr9uO+X9kQQRe7Lz6uxqxl5XKg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
@ -753,6 +762,11 @@
"wrap-ansi": "^7.0.0"
}
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -838,6 +852,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -1186,6 +1205,15 @@
"vary": "~1.1.2"
}
},
"express-validator": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.12.0.tgz",
"integrity": "sha512-lcQAdVeAO+pBbHD33nIsDsd+QPakLX08tJ82iEsXj6ezyWCfYjE9RY/g9SVq5z4G0NaIkH8039Oe4r0G92DRyA==",
"requires": {
"lodash": "^4.17.21",
"validator": "^13.5.2"
}
},
"eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
@ -1552,6 +1580,38 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
},
"ioredis": {
"version": "4.27.6",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.6.tgz",
"integrity": "sha512-6W3ZHMbpCa8ByMyC1LJGOi7P2WiOKP9B3resoZOVLDhi+6dDBOW+KNsRq3yI36Hmnb2sifCxHX+YSarTeXh48A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -1665,6 +1725,11 @@
"type-check": "~0.4.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -1676,6 +1741,16 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -1867,6 +1942,11 @@
"word-wrap": "^1.2.3"
}
},
"p-map": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
"integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -2104,6 +2184,24 @@
"util-deprecate": "^1.0.1"
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@ -2340,6 +2438,11 @@
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@ -2555,6 +2658,11 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"validator": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz",
"integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -7,15 +7,18 @@
"dotenv": "^10.0.0",
"env-var": "^7.0.1",
"express": "^4.17.1",
"express-validator": "^6.12.0",
"fabric-network": "^2.2.8",
"helmet": "^4.6.0",
"http-status-codes": "^2.1.4",
"ioredis": "^4.27.6",
"pino": "^6.11.3",
"pino-http": "^5.5.0",
"source-map-support": "^0.5.19"
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/ioredis": "^4.26.4",
"@types/node": "^15.12.4",
"@types/pino": "^6.3.8",
"@types/pino-http": "^5.4.1",
@ -38,6 +41,7 @@
"lint": "eslint . --ext .ts",
"start": "node --require source-map-support/register ./dist",
"start:dev": "node --require source-map-support/register --require dotenv/config ./dist | pino-pretty",
"start:redis": "docker run -p 6379:6379 --name fabric-sample-redis -d redis",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Hyperledger",

View file

@ -14,10 +14,24 @@ LOG_LEVEL=info
PORT=3000
CONNECTION_PROFILE=$(cat ${CONNECTION_PROFILE_FILE} | jq -c .)
RETRY_DELAY=3000
CERTIFICATE="$(cat ${CERTIFICATE_FILE} | sed -e 's/$/\\n/' | tr -d '\r\n')"
HLF_CONNECTION_PROFILE=$(cat ${CONNECTION_PROFILE_FILE} | jq -c .)
PRIVATE_KEY="$(cat ${PRIVATE_KEY_FILE} | sed -e 's/$/\\n/' | tr -d '\r\n')"
HLF_CERTIFICATE="$(cat ${CERTIFICATE_FILE} | sed -e 's/$/\\n/' | tr -d '\r\n')"
HLF_PRIVATE_KEY="$(cat ${PRIVATE_KEY_FILE} | sed -e 's/$/\\n/' | tr -d '\r\n')"
HLF_COMMIT_TIMEOUT=3000
HLF_ENDORSE_TIMEOUT=30
REDIS_HOST=localhost
REDIS_PORT=6379
#REDIS_USERNAME=
#REDIS_PASSWORD=
ENV_END

View file

@ -10,17 +10,101 @@
*
*/
import { Contract } from 'fabric-network';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import express, { Request, Response } from 'express';
import { body, validationResult } from 'express-validator';
import { Contract } from 'fabric-network';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { Redis } from 'ioredis';
import {
clearTransactionDetails,
createDeferredEventHandler,
storeTransactionDetails,
} from './fabric';
import { logger } from './logger';
const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
const { ACCEPTED, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } =
StatusCodes;
export const assetsRouter = express.Router();
assetsRouter.post(
'/',
body('id', 'must be a string').notEmpty(),
body('color', 'must be a string').notEmpty(),
body('size', 'must be a number').isNumeric(),
body('owner', 'must be a string').notEmpty(),
body('appraisedValue', 'must be a number').isNumeric(),
async (req: Request, res: Response) => {
logger.info(req.body, 'Create asset request received');
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST),
timestamp: new Date().toISOString(),
errors: errors.array(),
});
}
const contract: Contract = req.app.get('contract');
const redis: Redis = req.app.get('redis');
const txn = contract.createTransaction('CreateAsset');
const txnId = txn.getTransactionId();
const txnState = txn.serialize();
const txnArgs = JSON.stringify([
req.body.id,
req.body.color,
req.body.size,
req.body.owner,
req.body.appraisedValue,
]);
try {
const timestamp = Date.now();
// Store the transaction details and set the event handler in case there
// are problems later with commiting the transaction
await storeTransactionDetails(redis, txnId, txnState, txnArgs, timestamp);
txn.setEventHandler(createDeferredEventHandler(redis));
await txn.submit(
req.body.id,
req.body.color,
req.body.size,
req.body.owner,
req.body.appraisedValue
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
timestamp: new Date().toISOString(),
});
} catch (err) {
// TODO will this always catch endorsement errors or can those
// arrive later?
// There's no point retrying a transaction if there were business
// logic errors so clear the transaction details
//
// Note: it would be nice to pick out business logic errors returned
// from chaincode, e.g. asset already exists, and return those as a
// 400 error with message instead. Unfortunately the asset transfer
// sample or Fabric Node SDK do not provide any well defined error
// codes that can be checked.
await clearTransactionDetails(redis, txnId);
logger.error(err);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}
);
assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
logger.info(req.body, 'Read asset request received');
try {
const contract: Contract = req.app.get('contract');
@ -29,7 +113,7 @@ assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
const exists = data.toString() === 'true';
if (exists) {
res
return res
.status(OK)
.set({
Allow: 'GET,OPTIONS',
@ -39,7 +123,7 @@ assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
timestamp: new Date().toISOString(),
});
} else {
res.status(NOT_FOUND).json({
return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(),
});
@ -61,7 +145,7 @@ assetsRouter.get('/:assetId', async (req: Request, res: Response) => {
const data = await contract.evaluateTransaction('ReadAsset', assetId);
const asset = JSON.parse(data.toString());
res.status(OK).json(asset);
return res.status(OK).json(asset);
} catch (err) {
logger.error(err);
return res.status(INTERNAL_SERVER_ERROR).json({

View file

@ -15,6 +15,12 @@ export const port = env
.example('3000')
.asIntPositive();
export const retryDelay = env
.get('RETRY_DELAY')
.default('3000')
.example('3000')
.asIntPositive();
export const asLocalHost = env
.get('AS_LOCAL_HOST')
.default('true')
@ -24,25 +30,37 @@ export const asLocalHost = env
export const identityName = 'restServerIdentity';
export const mspId = env
.get('MSP_ID')
.get('HLF_MSP_ID')
.default('Org1MSP')
.example('Org1MSP')
.asString();
export const channelName = env
.get('CHANNEL_NAME')
.get('HLF_CHANNEL_NAME')
.default('mychannel')
.example('mychannel')
.asString();
export const chaincodeName = env
.get('CHAINCODE_NAME')
.get('HLF_CHAINCODE_NAME')
.default('basic')
.example('basic')
.asString();
export const commitTimeout = env
.get('HLF_COMMIT_TIMEOUT')
.default('3000')
.example('3000')
.asIntPositive();
export const endorseTimeout = env
.get('HLF_ENDORSE_TIMEOUT')
.default('30')
.example('30')
.asIntPositive();
export const connectionProfile = env
.get('CONNECTION_PROFILE')
.get('HLF_CONNECTION_PROFILE')
.required()
.example(
'{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }'
@ -50,13 +68,32 @@ export const connectionProfile = env
.asJsonObject();
export const certificate = env
.get('CERTIFICATE')
.get('HLF_CERTIFICATE')
.required()
.example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"')
.asString();
export const privateKey = env
.get('PRIVATE_KEY')
.get('HLF_PRIVATE_KEY')
.required()
.example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"')
.asString();
export const redisHost = env
.get('REDIS_HOST')
.default('localhost')
.example('localhost')
.asString();
export const redisPort = env
.get('REDIS_PORT')
.default('6379')
.example('6379')
.asIntPositive();
export const redisUsername = env
.get('REDIS_USERNAME')
.example('conga')
.asString();
export const redisPassword = env.get('REDIS_PASSWORD').asString();

View file

@ -3,14 +3,19 @@
*/
import {
CommitListener,
Contract,
DefaultEventHandlerStrategies,
DefaultQueryHandlerStrategies,
Gateway,
GatewayOptions,
Contract,
TxEventHandler,
TxEventHandlerFactory,
Wallets,
} from 'fabric-network';
import { Redis } from 'ioredis';
import * as config from './config';
import { logger } from './logger';
export const getContract = async (): Promise<Contract> => {
const wallet = await Wallets.newInMemoryWallet();
@ -31,6 +36,11 @@ export const getContract = async (): Promise<Contract> => {
wallet,
identity: config.identityName,
discovery: { enabled: true, asLocalhost: config.asLocalHost },
eventHandlerOptions: {
commitTimeout: config.commitTimeout,
endorseTimeout: config.endorseTimeout,
strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX,
},
queryHandlerOptions: {
timeout: 3,
strategy: DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN,
@ -44,3 +54,219 @@ export const getContract = async (): Promise<Contract> => {
return contract;
};
export const createDeferredEventHandler = (
redis: Redis
): TxEventHandlerFactory => {
return (transactionId, network): TxEventHandler => {
// TODO would like to store the transaction details here
// but doesn't seem possible to use await or handle errors
// in the TxEventHandlerFactory :(
const mspId = network.getGateway().getIdentity().mspId;
const peers = network.getChannel().getEndorsers(mspId);
const options = Object.assign(
{
commitTimeout: 30,
},
network.getGateway().getOptions().eventHandlerOptions
);
const removeCommitListener = async () => {
network.removeCommitListener(listener);
logger.info('Stopped listening for transaction %s events', transactionId);
const txnExists = await redis.exists(transactionId);
if (txnExists) {
logger.warn(
'Transaction %s was not successfully committed',
transactionId
);
}
};
const listener: CommitListener = async (error, event) => {
if (error) {
logger.error(error, 'Commit error for transaction %s', transactionId);
}
if (event && event.isValid) {
logger.info('Transaction %s successfully committed', transactionId);
await clearTransactionDetails(redis, transactionId);
await removeCommitListener();
}
};
const deferredEventHandler: TxEventHandler = {
startListening: async () => {
logger.info('Setting timeout for %d ms', options.commitTimeout * 1000);
setTimeout(async () => {
logger.info(
'Timeout listening for transaction %s events',
transactionId
);
await removeCommitListener();
}, options.commitTimeout * 1000);
await network.addCommitListener(listener, peers, transactionId);
logger.info('Listening for transaction %s events', transactionId);
},
waitForEvents: async () => {
// No-op
},
cancelListening: async () => {
// TODO this is what the doc says, but is it true?!
logger.info(
'Submission of transaction %s to the orderer failed',
transactionId
);
await removeCommitListener();
},
};
return deferredEventHandler;
};
};
export const startRetryLoop = (contract: Contract, redis: Redis): void => {
setInterval(
async (redis) => {
try {
const pendingTransactionCount = await (redis as Redis).zcard(
'index:txn:timestamp'
);
logger.info('Transactions awaiting retry: %d', pendingTransactionCount);
const transactionIds = await (redis as Redis).zrange(
'index:txn:timestamp',
-1,
-1
);
if (transactionIds.length > 0) {
const transactionId = transactionIds[0];
const savedTransaction = await (redis as Redis).hgetall(
`txn:${transactionId}`
);
await retryTransaction(
contract,
redis,
transactionId,
savedTransaction
);
}
} catch (err) {
// TODO just log?
logger.error(err, 'error getting saved transaction state');
}
},
config.retryDelay,
redis
);
};
const retryTransaction = async (
contract: Contract,
redis: Redis,
transactionId: string,
savedTransaction: Record<string, string>
) => {
logger.info('Retrying transaction %s', transactionId);
try {
const transaction = contract.deserializeTransaction(
Buffer.from(savedTransaction.state)
);
const args: string[] = JSON.parse(savedTransaction.args);
await transaction.submit(...args);
await clearTransactionDetails(redis, transactionId);
} catch (err) {
if (isDuplicateTransaction(err)) {
logger.info('Transaction %s has already been committed', transactionId);
await clearTransactionDetails(redis, transactionId);
} else {
// TODO check for retry limit and update timestamp
logger.warn(
err,
'Retry %d failed for transaction %s',
savedTransaction.retries,
transactionId
);
await (redis as Redis).hincrby(`txn:${transactionId}`, 'retries', 1);
}
}
};
const isDuplicateTransaction = (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;
};
// TODO move these to redis.ts?
export const storeTransactionDetails = async (
redis: Redis,
transactionId: string,
transactionState: Buffer,
transactionArgs: string,
timestamp: number
): Promise<void> => {
const key = `txn:${transactionId}`;
logger.info(
'Storing transaction details. Key: %s State: %s Args: %s Timestamp: %d',
key,
transactionState,
transactionArgs,
timestamp
);
await redis
.multi()
.hset(
key,
'state',
transactionState,
'args',
transactionArgs,
'timestamp',
timestamp,
'retries',
'0'
)
.zadd('index:txn:timestamp', timestamp, transactionId)
.exec();
};
export const clearTransactionDetails = async (
redis: Redis,
transactionId: string
): Promise<void> => {
const key = `txn:${transactionId}`;
logger.info('Removing transaction details. Key: %s', key);
try {
await redis
.multi()
.del(key)
.zrem('index:txn:timestamp', transactionId)
.exec();
} catch (err) {
logger.error(err, 'error remove saved transaction state');
}
};

View file

@ -2,16 +2,26 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Contract } from 'fabric-network';
import { Redis } from 'ioredis';
import * as config from './config';
import { startRetryLoop } from './fabric';
import { logger } from './logger';
import { createServer } from './server';
import * as config from './config';
async function main() {
const app = await createServer();
const contract: Contract = app.get('contract');
const redis: Redis = app.get('redis');
startRetryLoop(contract, redis);
app.listen(config.port, () => {
logger.info('Express server started on port: %d', config.port);
});
}
main();
// TODO handle errors! E.g. try starting with the wrong cert and private key!
main().catch((err) => {
logger.error(err, 'Unxepected error');
});

View file

@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
import IORedis, { RedisOptions } from 'ioredis';
import * as config from './config';
const redisOptions: RedisOptions = {
port: config.redisPort,
host: config.redisHost,
username: config.redisUsername,
password: config.redisPassword,
};
export const redis = new IORedis(redisOptions);

View file

@ -10,6 +10,7 @@ import pinoMiddleware from 'pino-http';
import { logger } from './logger';
import { assetsRouter } from './assets.router';
import { getContract } from './fabric';
import { redis } from './redis';
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
@ -49,6 +50,7 @@ export const createServer = async (): Promise<Application> => {
const contract = await getContract();
app.set('contract', contract);
app.set('redis', redis);
// Health routes
app.get('/ready', (_req, res) =>