mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
Initial create asset logic
Signed-off-by: James Taylor <jamest@uk.ibm.com>
This commit is contained in:
parent
324f1c8683
commit
3b50404763
10 changed files with 542 additions and 20 deletions
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
16
asset-transfer-basic/rest-api-typescript/src/redis.ts
Normal file
16
asset-transfer-basic/rest-api-typescript/src/redis.ts
Normal 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);
|
||||
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue