Update TypeScript implementations

- Dependency updates
- ESLint flat configuration format, replacing deprecated configuration
- Minor fixes to compile and lint issues
- Consistent TypeScript formatting with .editorconfig

Signed-off-by: Mark S. Lewis <Mark.S.Lewis@outlook.com>
This commit is contained in:
Mark S. Lewis 2024-06-15 14:33:30 +01:00 committed by Dave Enyeart
parent a4f0a2c5b2
commit c077dae79c
87 changed files with 6194 additions and 4254 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
root = true
[*]
charset = utf-8
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
indent_size = 4
quote_type = single
[*.json]
indent_size = 4
[*.md]
max_line_length = off

View file

@ -1,45 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"root": true,
"ignorePatterns": [
"dist/"
],
"extends": [
"eslint:recommended"
],
"rules": {
"indent": [
"error",
4
],
"quotes": [
"error",
"single"
]
},
"overrides": [
{
"files": [
"**/*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
]
}

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -34,11 +34,10 @@ const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051');
const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com'); const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com');
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
const assetId = `asset${Date.now()}`; const assetId = `asset${String(Date.now())}`;
async function main(): Promise<void> { async function main(): Promise<void> {
displayInputParameters();
await displayInputParameters();
// The gRPC client connection should be shared by all Gateway connections to this endpoint. // The gRPC client connection should be shared by all Gateway connections to this endpoint.
const client = await newGrpcConnection(); const client = await newGrpcConnection();
@ -92,7 +91,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -113,7 +112,11 @@ async function newIdentity(): Promise<Identity> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
return path.join(dirPath, files[0]); const file = files[0];
if (!file) {
throw new Error(`No files in directory: ${dirPath}`);
}
return path.join(dirPath, file);
} }
async function newSigner(): Promise<Signer> { async function newSigner(): Promise<Signer> {
@ -144,7 +147,7 @@ async function getAllAssets(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction('GetAllAssets'); const resultBytes = await contract.evaluateTransaction('GetAllAssets');
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -183,7 +186,7 @@ async function transferAssetAsync(contract: Contract): Promise<void> {
const status = await commit.getStatus(); const status = await commit.getStatus();
if (!status.successful) { if (!status.successful) {
throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`);
} }
console.log('*** Transaction committed successfully'); console.log('*** Transaction committed successfully');
@ -195,7 +198,7 @@ async function readAssetByID(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -230,7 +233,7 @@ function envOrDefault(key: string, defaultValue: string): string {
/** /**
* displayInputParameters() will print the global scope parameters used by the main driver routine. * displayInputParameters() will print the global scope parameters used by the main driver routine.
*/ */
async function displayInputParameters(): Promise<void> { function displayInputParameters(): void {
console.log(`channelName: ${channelName}`); console.log(`channelName: ${channelName}`);
console.log(`chaincodeName: ${chaincodeName}`); console.log(`chaincodeName: ${chaincodeName}`);
console.log(`mspId: ${mspId}`); console.log(`mspId: ${mspId}`);

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "outDir": "dist",
"emitDecoratorMetadata": true, "declaration": true,
"outDir": "dist", "declarationMap": true,
"declaration": true, "sourceMap": true,
"sourceMap": true, "noUnusedLocals": true,
"noImplicitAny": true "noImplicitReturns": true,
}, "noUncheckedIndexedAccess": true,
"include": [ "forceConsistentCasingInFileNames": true
"./src/**/*" },
], "include": ["./src/**/*"],
"exclude": [ "exclude": ["./src/**/*.spec.ts"]
"./src/**/*.spec.ts"
]
} }

View file

@ -10,17 +10,17 @@ export class Asset {
public docType?: string; public docType?: string;
@Property() @Property()
public ID: string; public ID: string = '';
@Property() @Property()
public Color: string; public Color: string = '';
@Property() @Property()
public Size: number; public Size: number = 0;
@Property() @Property()
public Owner: string; public Owner: string = '';
@Property() @Property()
public AppraisedValue: number; public AppraisedValue: number = 0;
} }

View file

@ -0,0 +1,3 @@
[*.ts]
indent_size = 4
quote_type = single

View file

@ -1,3 +0,0 @@
{
"singleQuote": true
}

View file

@ -4,205 +4,205 @@
*/ */
export default { export default {
// All imported modules in your tests should be mocked automatically // All imported modules in your tests should be mocked automatically
// automock: false, // automock: false,
// Stop running tests after `n` failures // Stop running tests after `n` failures
// bail: 0, // bail: 0,
// The directory where Jest should store its cached dependency information // The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/04/rqvxpdk52gvf1_qq9l8gt4d40000gn/T/jest_dx", // cacheDirectory: "/private/var/folders/04/rqvxpdk52gvf1_qq9l8gt4d40000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test // Automatically clear mock calls and instances between every test
clearMocks: true, clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test // Indicates whether the coverage information should be collected while executing the test
collectCoverage: true, collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected // An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined, // collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files // The directory where Jest should output its coverage files
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection // An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [ // coveragePathIgnorePatterns: [
// "/node_modules/" // "/node_modules/"
// ], // ],
// Indicates which provider should be used to instrument code for coverage // Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8', coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports // A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [ // coverageReporters: [
// "json", // "json",
// "text", // "text",
// "lcov", // "lcov",
// "clover" // "clover"
// ], // ],
// An object that configures minimum threshold enforcement for coverage results // An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined, // coverageThreshold: undefined,
// A path to a custom dependency extractor // A path to a custom dependency extractor
// dependencyExtractor: undefined, // dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages // Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false, // errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns // Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [], // forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites // A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined, // globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites // A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined, // globalTeardown: undefined,
// A set of global variables that need to be available in all test environments // A set of global variables that need to be available in all test environments
// globals: {}, // globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%", // maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location // An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [ // moduleDirectories: [
// "node_modules" // "node_modules"
// ], // ],
// An array of file extensions your modules use // An array of file extensions your modules use
// moduleFileExtensions: [ // moduleFileExtensions: [
// "js", // "js",
// "jsx", // "jsx",
// "ts", // "ts",
// "tsx", // "tsx",
// "json", // "json",
// "node" // "node"
// ], // ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {}, // moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [], // modulePathIgnorePatterns: [],
// Activates notifications for test results // Activates notifications for test results
// notify: false, // notify: false,
// An enum that specifies notification mode. Requires { notify: true } // An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change", // notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration // A preset that is used as a base for Jest's configuration
preset: 'ts-jest', preset: 'ts-jest',
// Run tests from one or more projects // Run tests from one or more projects
// projects: undefined, // projects: undefined,
// Use this configuration option to add custom reporters to Jest // Use this configuration option to add custom reporters to Jest
// reporters: undefined, // reporters: undefined,
// Automatically reset mock state between every test // Automatically reset mock state between every test
// resetMocks: false, // resetMocks: false,
// Reset the module registry before running each individual test // Reset the module registry before running each individual test
// resetModules: false, // resetModules: false,
// A path to a custom resolver // A path to a custom resolver
// resolver: undefined, // resolver: undefined,
// Automatically restore mock state between every test // Automatically restore mock state between every test
// restoreMocks: false, // restoreMocks: false,
// The root directory that Jest should scan for tests and modules within // The root directory that Jest should scan for tests and modules within
// rootDir: undefined, // rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in // A list of paths to directories that Jest should use to search for files in
roots: ['<rootDir>/src'], roots: ['<rootDir>/src'],
// Allows you to use a custom runner instead of Jest's default test runner // Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner", // runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test // The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [], // setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test // A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [], // setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results. // The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5, // slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing // A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [], // snapshotSerializers: [],
// The test environment that will be used for testing // The test environment that will be used for testing
// testEnvironment: "jest-environment-node", // testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment // Options that will be passed to the testEnvironment
// testEnvironmentOptions: {}, // testEnvironmentOptions: {},
// Adds a location field to test results // Adds a location field to test results
// testLocationInResults: false, // testLocationInResults: false,
// The glob patterns Jest uses to detect test files // The glob patterns Jest uses to detect test files
testMatch: [ testMatch: [
// "**/__tests__/**/*.[jt]s?(x)", // "**/__tests__/**/*.[jt]s?(x)",
'**/?(*.)+(spec|test).[tj]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)',
], ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [ // testPathIgnorePatterns: [
// "/node_modules/" // "/node_modules/"
// ], // ],
// The regexp pattern or array of patterns that Jest uses to detect test files // The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [], // testRegex: [],
// This option allows the use of a custom results processor // This option allows the use of a custom results processor
// testResultsProcessor: undefined, // testResultsProcessor: undefined,
// This option allows use of a custom test runner // This option allows use of a custom test runner
// testRunner: "jest-circus/runner", // testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost", // testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real", // timers: "real",
// A map from regular expressions to paths to transformers // A map from regular expressions to paths to transformers
// transform: undefined, // transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [ // transformIgnorePatterns: [
// "/node_modules/", // "/node_modules/",
// "\\.pnp\\.[^\\/]+$" // "\\.pnp\\.[^\\/]+$"
// ], // ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined, // unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run // Indicates whether each individual test should be reported during the run
// verbose: undefined, // verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [], // watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling // Whether to use watchman for file crawling
// watchman: true, // watchman: true,
}; };
// Required environment variable values for the config.ts file // Required environment variable values for the config.ts file
process.env = Object.assign(process.env, { process.env = Object.assign(process.env, {
HLF_CONNECTION_PROFILE_ORG1: '{"name":"mock-profile-org1"}', HLF_CONNECTION_PROFILE_ORG1: '{"name":"mock-profile-org1"}',
HLF_CERTIFICATE_ORG1: HLF_CERTIFICATE_ORG1:
'"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"',
HLF_PRIVATE_KEY_ORG1: HLF_PRIVATE_KEY_ORG1:
'"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"',
HLF_CONNECTION_PROFILE_ORG2: '{"name":"mock-profile-org2"}', HLF_CONNECTION_PROFILE_ORG2: '{"name":"mock-profile-org2"}',
HLF_CERTIFICATE_ORG2: HLF_CERTIFICATE_ORG2:
'"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"', '"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"',
HLF_PRIVATE_KEY_ORG2: HLF_PRIVATE_KEY_ORG2:
'"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"', '"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"',
ORG1_APIKEY: 'ORG1MOCKAPIKEY', ORG1_APIKEY: 'ORG1MOCKAPIKEY',
ORG2_APIKEY: 'ORG2MOCKAPIKEY', ORG2_APIKEY: 'ORG2MOCKAPIKEY',
}); });

View file

@ -29,319 +29,325 @@ import { addSubmitTransactionJob } from './jobs';
import { logger } from './logger'; import { logger } from './logger';
const { ACCEPTED, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = const { ACCEPTED, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } =
StatusCodes; StatusCodes;
export const assetsRouter = express.Router(); export const assetsRouter = express.Router();
assetsRouter.get('/', async (req: Request, res: Response) => { assetsRouter.get('/', async (req: Request, res: Response) => {
logger.debug('Get all assets request received'); logger.debug('Get all assets request received');
try { try {
const mspId = req.user as string; const mspId = req.user as string;
const contract = req.app.locals[mspId]?.assetContract as Contract; const contract = req.app.locals[mspId]?.assetContract as Contract;
const data = await evatuateTransaction(contract, 'GetAllAssets'); const data = await evatuateTransaction(contract, 'GetAllAssets');
let assets = []; let assets = [];
if (data.length > 0) { if (data.length > 0) {
assets = JSON.parse(data.toString()); assets = JSON.parse(data.toString());
}
return res.status(OK).json(assets);
} catch (err) {
logger.error({ err }, 'Error processing get all assets request');
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
} }
return res.status(OK).json(assets);
} catch (err) {
logger.error({ err }, 'Error processing get all assets request');
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}); });
assetsRouter.post( assetsRouter.post(
'/', '/',
body().isObject().withMessage('body must contain an asset object'), body().isObject().withMessage('body must contain an asset object'),
body('ID', 'must be a string').notEmpty(), body('ID', 'must be a string').notEmpty(),
body('Color', 'must be a string').notEmpty(), body('Color', 'must be a string').notEmpty(),
body('Size', 'must be a number').isNumeric(), body('Size', 'must be a number').isNumeric(),
body('Owner', 'must be a string').notEmpty(), body('Owner', 'must be a string').notEmpty(),
body('AppraisedValue', 'must be a number').isNumeric(), body('AppraisedValue', 'must be a number').isNumeric(),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
logger.debug(req.body, 'Create asset request received'); logger.debug(req.body, 'Create asset request received');
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
return res.status(BAD_REQUEST).json({ return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST), status: getReasonPhrase(BAD_REQUEST),
reason: 'VALIDATION_ERROR', reason: 'VALIDATION_ERROR',
message: 'Invalid request body', message: 'Invalid request body',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
errors: errors.array(), errors: errors.array(),
}); });
}
const mspId = req.user as string;
const assetId = req.body.ID;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'CreateAsset',
assetId,
req.body.Color,
req.body.Size,
req.body.Owner,
req.body.AppraisedValue
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing create asset request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
} }
const mspId = req.user as string;
const assetId = req.body.ID;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'CreateAsset',
assetId,
req.body.Color,
req.body.Size,
req.body.Owner,
req.body.AppraisedValue
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing create asset request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}
); );
assetsRouter.options('/:assetId', async (req: Request, res: Response) => { assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
const assetId = req.params.assetId; const assetId = req.params.assetId;
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 mspId = req.user as string; const mspId = req.user as string;
const contract = req.app.locals[mspId]?.assetContract as Contract; const contract = req.app.locals[mspId]?.assetContract as Contract;
const data = await evatuateTransaction(contract, 'AssetExists', assetId); const data = await evatuateTransaction(
const exists = data.toString() === 'true'; contract,
'AssetExists',
assetId
);
const exists = data.toString() === 'true';
if (exists) { if (exists) {
return res return res
.status(OK) .status(OK)
.set({ .set({
Allow: 'DELETE,GET,OPTIONS,PATCH,PUT', Allow: 'DELETE,GET,OPTIONS,PATCH,PUT',
}) })
.json({ .json({
status: getReasonPhrase(OK), status: getReasonPhrase(OK),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
});
} else {
return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(),
});
}
} catch (err) {
logger.error(
{ err },
'Error processing asset options request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
}); });
} else {
return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(),
});
} }
} catch (err) {
logger.error(
{ err },
'Error processing asset options request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}); });
assetsRouter.get('/:assetId', async (req: Request, res: Response) => { assetsRouter.get('/:assetId', async (req: Request, res: Response) => {
const assetId = req.params.assetId; const assetId = req.params.assetId;
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 mspId = req.user as string; const mspId = req.user as string;
const contract = req.app.locals[mspId]?.assetContract as Contract; const contract = req.app.locals[mspId]?.assetContract as 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());
return res.status(OK).json(asset); return res.status(OK).json(asset);
} catch (err) { } catch (err) {
logger.error( logger.error(
{ err }, { err },
'Error processing read asset request for asset ID %s', 'Error processing read asset request for asset ID %s',
assetId assetId
); );
if (err instanceof AssetNotFoundError) { if (err instanceof AssetNotFoundError) {
return res.status(NOT_FOUND).json({ return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND), status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
} }
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}); });
assetsRouter.put( assetsRouter.put(
'/:assetId', '/:assetId',
body().isObject().withMessage('body must contain an asset object'), body().isObject().withMessage('body must contain an asset object'),
body('ID', 'must be a string').notEmpty(), body('ID', 'must be a string').notEmpty(),
body('Color', 'must be a string').notEmpty(), body('Color', 'must be a string').notEmpty(),
body('Size', 'must be a number').isNumeric(), body('Size', 'must be a number').isNumeric(),
body('Owner', 'must be a string').notEmpty(), body('Owner', 'must be a string').notEmpty(),
body('AppraisedValue', 'must be a number').isNumeric(), body('AppraisedValue', 'must be a number').isNumeric(),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
logger.debug(req.body, 'Update asset request received'); logger.debug(req.body, 'Update asset request received');
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
return res.status(BAD_REQUEST).json({ return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST), status: getReasonPhrase(BAD_REQUEST),
reason: 'VALIDATION_ERROR', reason: 'VALIDATION_ERROR',
message: 'Invalid request body', message: 'Invalid request body',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
errors: errors.array(), errors: errors.array(),
}); });
}
if (req.params.assetId != req.body.ID) {
return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST),
reason: 'ASSET_ID_MISMATCH',
message: 'Asset IDs must match',
timestamp: new Date().toISOString(),
});
}
const mspId = req.user as string;
const assetId = req.params.assetId;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'UpdateAsset',
assetId,
req.body.color,
req.body.size,
req.body.owner,
req.body.appraisedValue
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing update asset request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
} }
if (req.params.assetId != req.body.ID) {
return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST),
reason: 'ASSET_ID_MISMATCH',
message: 'Asset IDs must match',
timestamp: new Date().toISOString(),
});
}
const mspId = req.user as string;
const assetId = req.params.assetId;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'UpdateAsset',
assetId,
req.body.color,
req.body.size,
req.body.owner,
req.body.appraisedValue
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing update asset request for asset ID %s',
assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}
); );
assetsRouter.patch( assetsRouter.patch(
'/:assetId', '/:assetId',
body() body()
.isArray({ .isArray({
min: 1, min: 1,
max: 1, max: 1,
}) })
.withMessage('body must contain an array with a single patch operation'), .withMessage(
body('*.op', "operation must be 'replace'").equals('replace'), 'body must contain an array with a single patch operation'
body('*.path', "path must be '/Owner'").equals('/Owner'), ),
body('*.value', 'must be a string').isString(), body('*.op', "operation must be 'replace'").equals('replace'),
async (req: Request, res: Response) => { body('*.path', "path must be '/Owner'").equals('/Owner'),
logger.debug(req.body, 'Transfer asset request received'); body('*.value', 'must be a string').isString(),
async (req: Request, res: Response) => {
logger.debug(req.body, 'Transfer asset request received');
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
return res.status(BAD_REQUEST).json({ return res.status(BAD_REQUEST).json({
status: getReasonPhrase(BAD_REQUEST), status: getReasonPhrase(BAD_REQUEST),
reason: 'VALIDATION_ERROR', reason: 'VALIDATION_ERROR',
message: 'Invalid request body', message: 'Invalid request body',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
errors: errors.array(), errors: errors.array(),
}); });
}
const mspId = req.user as string;
const assetId = req.params.assetId;
const newOwner = req.body[0].value;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'TransferAsset',
assetId,
newOwner
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing update asset request for asset ID %s',
req.params.assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
} }
const mspId = req.user as string;
const assetId = req.params.assetId;
const newOwner = req.body[0].value;
try {
const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob(
submitQueue,
mspId,
'TransferAsset',
assetId,
newOwner
);
return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED),
jobId: jobId,
timestamp: new Date().toISOString(),
});
} catch (err) {
logger.error(
{ err },
'Error processing update asset request for asset ID %s',
req.params.assetId
);
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}
); );
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 mspId = req.user as string; const mspId = req.user as string;
const assetId = req.params.assetId; const assetId = req.params.assetId;
try { try {
const submitQueue = req.app.locals.jobq as Queue; const submitQueue = req.app.locals.jobq as Queue;
const jobId = await addSubmitTransactionJob( const jobId = await addSubmitTransactionJob(
submitQueue, submitQueue,
mspId, mspId,
'DeleteAsset', 'DeleteAsset',
assetId assetId
); );
return res.status(ACCEPTED).json({ return res.status(ACCEPTED).json({
status: getReasonPhrase(ACCEPTED), status: getReasonPhrase(ACCEPTED),
jobId: jobId, jobId: jobId,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} catch (err) { } catch (err) {
logger.error( logger.error(
{ err }, { err },
'Error processing delete asset request for asset ID %s', 'Error processing delete asset request for asset ID %s',
assetId assetId
); );
return res.status(INTERNAL_SERVER_ERROR).json({ return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR), status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
} }
}); });

View file

@ -12,50 +12,50 @@ import * as config from './config';
const { UNAUTHORIZED } = StatusCodes; const { UNAUTHORIZED } = StatusCodes;
export const fabricAPIKeyStrategy: HeaderAPIKeyStrategy = export const fabricAPIKeyStrategy: HeaderAPIKeyStrategy =
new HeaderAPIKeyStrategy( new HeaderAPIKeyStrategy(
{ header: 'X-API-Key', prefix: '' }, { header: 'X-API-Key', prefix: '' },
false, false,
function (apikey, done) { function (apikey, done) {
logger.debug({ apikey }, 'Checking X-API-Key'); logger.debug({ apikey }, 'Checking X-API-Key');
if (apikey === config.org1ApiKey) { if (apikey === config.org1ApiKey) {
const user = config.mspIdOrg1; const user = config.mspIdOrg1;
logger.debug('User set to %s', user); logger.debug('User set to %s', user);
done(null, user); done(null, user);
} else if (apikey === config.org2ApiKey) { } else if (apikey === config.org2ApiKey) {
const user = config.mspIdOrg2; const user = config.mspIdOrg2;
logger.debug('User set to %s', user); logger.debug('User set to %s', user);
done(null, user); done(null, user);
} else { } else {
logger.debug({ apikey }, 'No valid X-API-Key'); logger.debug({ apikey }, 'No valid X-API-Key');
return done(null, false); return done(null, false);
} }
} }
); );
export const authenticateApiKey = ( export const authenticateApiKey = (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): void => { ): void => {
passport.authenticate( passport.authenticate(
'headerapikey', 'headerapikey',
{ session: false }, { session: false },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(err: any, user: Express.User, _info: any) => { (err: any, user: Express.User, _info: any) => {
if (err) return next(err); if (err) return next(err);
if (!user) if (!user)
return res.status(UNAUTHORIZED).json({ return res.status(UNAUTHORIZED).json({
status: getReasonPhrase(UNAUTHORIZED), status: getReasonPhrase(UNAUTHORIZED),
reason: 'NO_VALID_APIKEY', reason: 'NO_VALID_APIKEY',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
req.logIn(user, { session: false }, async (err) => { req.logIn(user, { session: false }, async (err) => {
if (err) { if (err) {
return next(err); return next(err);
}
return next();
});
} }
return next(); )(req, res, next);
});
}
)(req, res, next);
}; };

File diff suppressed because it is too large Load diff

View file

@ -24,71 +24,71 @@ export const JOB_QUEUE_NAME = 'submit';
* Log level for the REST server * Log level for the REST server
*/ */
export const logLevel = env export const logLevel = env
.get('LOG_LEVEL') .get('LOG_LEVEL')
.default('info') .default('info')
.asEnum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']); .asEnum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
/** /**
* The port to start the REST server on * The port to start the REST server on
*/ */
export const port = env export const port = env
.get('PORT') .get('PORT')
.default('3000') .default('3000')
.example('3000') .example('3000')
.asPortNumber(); .asPortNumber();
/** /**
* The type of backoff to use for retrying failed submit jobs * The type of backoff to use for retrying failed submit jobs
*/ */
export const submitJobBackoffType = env export const submitJobBackoffType = env
.get('SUBMIT_JOB_BACKOFF_TYPE') .get('SUBMIT_JOB_BACKOFF_TYPE')
.default('fixed') .default('fixed')
.asEnum(['fixed', 'exponential']); .asEnum(['fixed', 'exponential']);
/** /**
* Backoff delay for retrying failed submit jobs in milliseconds * Backoff delay for retrying failed submit jobs in milliseconds
*/ */
export const submitJobBackoffDelay = env export const submitJobBackoffDelay = env
.get('SUBMIT_JOB_BACKOFF_DELAY') .get('SUBMIT_JOB_BACKOFF_DELAY')
.default('3000') .default('3000')
.example('3000') .example('3000')
.asIntPositive(); .asIntPositive();
/** /**
* The total number of attempts to try a submit job until it completes * The total number of attempts to try a submit job until it completes
*/ */
export const submitJobAttempts = env export const submitJobAttempts = env
.get('SUBMIT_JOB_ATTEMPTS') .get('SUBMIT_JOB_ATTEMPTS')
.default('5') .default('5')
.example('5') .example('5')
.asIntPositive(); .asIntPositive();
/** /**
* The maximum number of submit jobs that can be processed in parallel * The maximum number of submit jobs that can be processed in parallel
*/ */
export const submitJobConcurrency = env export const submitJobConcurrency = env
.get('SUBMIT_JOB_CONCURRENCY') .get('SUBMIT_JOB_CONCURRENCY')
.default('5') .default('5')
.example('5') .example('5')
.asIntPositive(); .asIntPositive();
/** /**
* The number of completed submit jobs to keep * The number of completed submit jobs to keep
*/ */
export const maxCompletedSubmitJobs = env export const maxCompletedSubmitJobs = env
.get('MAX_COMPLETED_SUBMIT_JOBS') .get('MAX_COMPLETED_SUBMIT_JOBS')
.default('1000') .default('1000')
.example('1000') .example('1000')
.asIntPositive(); .asIntPositive();
/** /**
* The number of failed submit jobs to keep * The number of failed submit jobs to keep
*/ */
export const maxFailedSubmitJobs = env export const maxFailedSubmitJobs = env
.get('MAX_FAILED_SUBMIT_JOBS') .get('MAX_FAILED_SUBMIT_JOBS')
.default('1000') .default('1000')
.example('1000') .example('1000')
.asIntPositive(); .asIntPositive();
/** /**
* Whether to initialise a scheduler for the submit job queue * Whether to initialise a scheduler for the submit job queue
@ -96,10 +96,10 @@ export const maxFailedSubmitJobs = env
* more than one for redundancy * more than one for redundancy
*/ */
export const submitJobQueueScheduler = env export const submitJobQueueScheduler = env
.get('SUBMIT_JOB_QUEUE_SCHEDULER') .get('SUBMIT_JOB_QUEUE_SCHEDULER')
.default('true') .default('true')
.example('true') .example('true')
.asBoolStrict(); .asBoolStrict();
/** /**
* Whether to convert discovered host addresses to be 'localhost' * Whether to convert discovered host addresses to be 'localhost'
@ -107,157 +107,165 @@ export const submitJobQueueScheduler = env
* local system, e.g. using the test network; otherwise should it should be 'false' * local system, e.g. using the test network; otherwise should it should be 'false'
*/ */
export const asLocalhost = env export const asLocalhost = env
.get('AS_LOCAL_HOST') .get('AS_LOCAL_HOST')
.default('true') .default('true')
.example('true') .example('true')
.asBoolStrict(); .asBoolStrict();
/** /**
* The Org1 MSP ID * The Org1 MSP ID
*/ */
export const mspIdOrg1 = env export const mspIdOrg1 = env
.get('HLF_MSP_ID_ORG1') .get('HLF_MSP_ID_ORG1')
.default(`${ORG1}MSP`) .default(`${ORG1}MSP`)
.example(`${ORG1}MSP`) .example(`${ORG1}MSP`)
.asString(); .asString();
/** /**
* The Org2 MSP ID * The Org2 MSP ID
*/ */
export const mspIdOrg2 = env export const mspIdOrg2 = env
.get('HLF_MSP_ID_ORG2') .get('HLF_MSP_ID_ORG2')
.default(`${ORG2}MSP`) .default(`${ORG2}MSP`)
.example(`${ORG2}MSP`) .example(`${ORG2}MSP`)
.asString(); .asString();
/** /**
* Name of the channel which the basic asset sample chaincode has been installed on * Name of the channel which the basic asset sample chaincode has been installed on
*/ */
export const channelName = env export const channelName = env
.get('HLF_CHANNEL_NAME') .get('HLF_CHANNEL_NAME')
.default('mychannel') .default('mychannel')
.example('mychannel') .example('mychannel')
.asString(); .asString();
/** /**
* Name used to install the basic asset sample * Name used to install the basic asset sample
*/ */
export const chaincodeName = env export const chaincodeName = env
.get('HLF_CHAINCODE_NAME') .get('HLF_CHAINCODE_NAME')
.default('basic') .default('basic')
.example('basic') .example('basic')
.asString(); .asString();
/** /**
* The transaction submit timeout in seconds for commit notification to complete * The transaction submit timeout in seconds for commit notification to complete
*/ */
export const commitTimeout = env export const commitTimeout = env
.get('HLF_COMMIT_TIMEOUT') .get('HLF_COMMIT_TIMEOUT')
.default('300') .default('300')
.example('300') .example('300')
.asIntPositive(); .asIntPositive();
/** /**
* The transaction submit timeout in seconds for the endorsement to complete * The transaction submit timeout in seconds for the endorsement to complete
*/ */
export const endorseTimeout = env export const endorseTimeout = env
.get('HLF_ENDORSE_TIMEOUT') .get('HLF_ENDORSE_TIMEOUT')
.default('30') .default('30')
.example('30') .example('30')
.asIntPositive(); .asIntPositive();
/** /**
* The transaction query timeout in seconds * The transaction query timeout in seconds
*/ */
export const queryTimeout = env export const queryTimeout = env
.get('HLF_QUERY_TIMEOUT') .get('HLF_QUERY_TIMEOUT')
.default('3') .default('3')
.example('3') .example('3')
.asIntPositive(); .asIntPositive();
/** /**
* The Org1 connection profile JSON * The Org1 connection profile JSON
*/ */
export const connectionProfileOrg1 = env export const connectionProfileOrg1 = env
.get('HLF_CONNECTION_PROFILE_ORG1') .get('HLF_CONNECTION_PROFILE_ORG1')
.required() .required()
.example( .example(
'{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }' '{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }'
) )
.asJsonObject() as Record<string, unknown>; .asJsonObject() as Record<string, unknown>;
/** /**
* Certificate for an Org1 identity to evaluate and submit transactions * Certificate for an Org1 identity to evaluate and submit transactions
*/ */
export const certificateOrg1 = env export const certificateOrg1 = env
.get('HLF_CERTIFICATE_ORG1') .get('HLF_CERTIFICATE_ORG1')
.required() .required()
.example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') .example(
.asString(); '"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
)
.asString();
/** /**
* Private key for an Org1 identity to evaluate and submit transactions * Private key for an Org1 identity to evaluate and submit transactions
*/ */
export const privateKeyOrg1 = env export const privateKeyOrg1 = env
.get('HLF_PRIVATE_KEY_ORG1') .get('HLF_PRIVATE_KEY_ORG1')
.required() .required()
.example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') .example(
.asString(); '"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
)
.asString();
/** /**
* The Org2 connection profile JSON * The Org2 connection profile JSON
*/ */
export const connectionProfileOrg2 = env export const connectionProfileOrg2 = env
.get('HLF_CONNECTION_PROFILE_ORG2') .get('HLF_CONNECTION_PROFILE_ORG2')
.required() .required()
.example( .example(
'{"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }' '{"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }'
) )
.asJsonObject() as Record<string, unknown>; .asJsonObject() as Record<string, unknown>;
/** /**
* Certificate for an Org2 identity to evaluate and submit transactions * Certificate for an Org2 identity to evaluate and submit transactions
*/ */
export const certificateOrg2 = env export const certificateOrg2 = env
.get('HLF_CERTIFICATE_ORG2') .get('HLF_CERTIFICATE_ORG2')
.required() .required()
.example('"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"') .example(
.asString(); '"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
)
.asString();
/** /**
* Private key for an Org2 identity to evaluate and submit transactions * Private key for an Org2 identity to evaluate and submit transactions
*/ */
export const privateKeyOrg2 = env export const privateKeyOrg2 = env
.get('HLF_PRIVATE_KEY_ORG2') .get('HLF_PRIVATE_KEY_ORG2')
.required() .required()
.example('"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"') .example(
.asString(); '"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
)
.asString();
/** /**
* The host the Redis server is running on * The host the Redis server is running on
*/ */
export const redisHost = env export const redisHost = env
.get('REDIS_HOST') .get('REDIS_HOST')
.default('localhost') .default('localhost')
.example('localhost') .example('localhost')
.asString(); .asString();
/** /**
* The port the Redis server is running on * The port the Redis server is running on
*/ */
export const redisPort = env export const redisPort = env
.get('REDIS_PORT') .get('REDIS_PORT')
.default('6379') .default('6379')
.example('6379') .example('6379')
.asPortNumber(); .asPortNumber();
/** /**
* Username for the Redis server * Username for the Redis server
*/ */
export const redisUsername = env export const redisUsername = env
.get('REDIS_USERNAME') .get('REDIS_USERNAME')
.example('fabric') .example('fabric')
.asString(); .asString();
/** /**
* Password for the Redis server * Password for the Redis server
@ -269,17 +277,17 @@ export const redisPassword = env.get('REDIS_PASSWORD').asString();
* Specify this API key with the X-Api-Key header to use the Org1 connection profile and credentials * Specify this API key with the X-Api-Key header to use the Org1 connection profile and credentials
*/ */
export const org1ApiKey = env export const org1ApiKey = env
.get('ORG1_APIKEY') .get('ORG1_APIKEY')
.required() .required()
.example('123') .example('123')
.asString(); .asString();
/** /**
* API key for Org2 * API key for Org2
* Specify this API key with the X-Api-Key header to use the Org2 connection profile and credentials * Specify this API key with the X-Api-Key header to use the Org2 connection profile and credentials
*/ */
export const org2ApiKey = env export const org2ApiKey = env
.get('ORG2_APIKEY') .get('ORG2_APIKEY')
.required() .required()
.example('456') .example('456')
.asString(); .asString();

View file

@ -4,311 +4,326 @@
import { TimeoutError, TransactionError } from 'fabric-network'; import { TimeoutError, TransactionError } from 'fabric-network';
import { import {
AssetExistsError, AssetExistsError,
AssetNotFoundError, AssetNotFoundError,
TransactionNotFoundError, TransactionNotFoundError,
getRetryAction, getRetryAction,
handleError, handleError,
isDuplicateTransactionError, isDuplicateTransactionError,
isErrorLike, isErrorLike,
RetryAction, RetryAction,
} from './errors'; } from './errors';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
describe('Errors', () => { describe('Errors', () => {
describe('isErrorLike', () => { describe('isErrorLike', () => {
it('returns false for null', () => { it('returns false for null', () => {
expect(isErrorLike(null)).toBe(false); expect(isErrorLike(null)).toBe(false);
});
it('returns false for undefined', () => {
expect(isErrorLike(undefined)).toBe(false);
});
it('returns false for empty object', () => {
expect(isErrorLike({})).toBe(false);
});
it('returns false for string', () => {
expect(isErrorLike('true')).toBe(false);
});
it('returns false for non-error object', () => {
expect(isErrorLike({ size: 42 })).toBe(false);
});
it('returns false for invalid error object', () => {
expect(isErrorLike({ name: 'MockError', message: 42 })).toBe(false);
});
it('returns false for error like object with invalid stack', () => {
expect(
isErrorLike({
name: 'MockError',
message: 'Fail',
stack: false,
})
).toBe(false);
});
it('returns true for error like object', () => {
expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(
true
);
});
it('returns true for new Error', () => {
expect(isErrorLike(new Error('Error'))).toBe(true);
});
}); });
it('returns false for undefined', () => { describe('isDuplicateTransactionError', () => {
expect(isErrorLike(undefined)).toBe(false); it('returns true for a TransactionError with a transaction code of DUPLICATE_TXID', () => {
const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID';
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(true);
});
it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => {
const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode =
'MVCC_READ_CONFLICT';
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(false);
});
it('returns true for an error when all endorsement details are duplicate transaction found', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(true);
});
it('returns true for an error when at least one endorsement details are duplicate transaction found', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'mock endorsement details',
},
{
details: 'mock endorsement details',
},
],
},
],
};
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(true);
});
it('returns false for an error without duplicate transaction endorsement details', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'mock endorsement details',
},
{
details: 'mock endorsement details',
},
],
},
],
};
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(false);
});
it('returns false for an error without endorsement details', () => {
const mockDuplicateTransactionError = {
errors: [
{
rejections: [
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(
isDuplicateTransactionError(mockDuplicateTransactionError)
).toBe(false);
});
it('returns false for a basic Error object without endorsement details', () => {
expect(
isDuplicateTransactionError(
new Error('duplicate transaction found')
)
).toBe(false);
});
it('returns false for an undefined error', () => {
expect(isDuplicateTransactionError(undefined)).toBe(false);
});
it('returns false for a null error', () => {
expect(isDuplicateTransactionError(null)).toBe(false);
});
}); });
it('returns false for empty object', () => { describe('getRetryAction', () => {
expect(isErrorLike({})).toBe(false); it('returns RetryAction.None for duplicate transaction errors', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(getRetryAction(mockDuplicateTransactionError)).toBe(
RetryAction.None
);
});
it('returns RetryAction.None for a TransactionNotFoundError', () => {
const mockTransactionNotFoundError = new TransactionNotFoundError(
'Failed to get transaction with id txn, error Entry not found in index',
'txn1'
);
expect(getRetryAction(mockTransactionNotFoundError)).toBe(
RetryAction.None
);
});
it('returns RetryAction.None for an AssetExistsError', () => {
const mockAssetExistsError = new AssetExistsError(
'The asset MOCK_ASSET already exists',
'txn1'
);
expect(getRetryAction(mockAssetExistsError)).toBe(RetryAction.None);
});
it('returns RetryAction.None for an AssetNotFoundError', () => {
const mockAssetNotFoundError = new AssetNotFoundError(
'the asset MOCK_ASSET does not exist',
'txn1'
);
expect(getRetryAction(mockAssetNotFoundError)).toBe(
RetryAction.None
);
});
it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => {
const mockTimeoutError = new TimeoutError('MOCK TIMEOUT ERROR');
expect(getRetryAction(mockTimeoutError)).toBe(
RetryAction.WithExistingTransactionId
);
});
it('returns RetryAction.WithNewTransactionId for an MVCC_READ_CONFLICT TransactionError', () => {
const mockTransactionError = mock<TransactionError>();
mockTransactionError.transactionCode = 'MVCC_READ_CONFLICT';
expect(getRetryAction(mockTransactionError)).toBe(
RetryAction.WithNewTransactionId
);
});
it('returns RetryAction.WithNewTransactionId for an Error', () => {
const mockError = new Error('MOCK ERROR');
expect(getRetryAction(mockError)).toBe(
RetryAction.WithNewTransactionId
);
});
it('returns RetryAction.WithNewTransactionId for a string error', () => {
const mockError = 'MOCK ERROR';
expect(getRetryAction(mockError)).toBe(
RetryAction.WithNewTransactionId
);
});
}); });
it('returns false for string', () => { describe('handleError', () => {
expect(isErrorLike('true')).toBe(false); it.each([
}); 'the asset GOCHAINCODE already exists',
'Asset JAVACHAINCODE already exists',
it('returns false for non-error object', () => { 'The asset JSCHAINCODE already exists',
expect(isErrorLike({ size: 42 })).toBe(false); ])(
}); 'returns a AssetExistsError for errors with an asset already exists message: %s',
(msg) => {
it('returns false for invalid error object', () => { expect(handleError('txn1', new Error(msg))).toStrictEqual(
expect(isErrorLike({ name: 'MockError', message: 42 })).toBe(false); new AssetExistsError(msg, 'txn1')
}); );
}
it('returns false for error like object with invalid stack', () => {
expect(
isErrorLike({ name: 'MockError', message: 'Fail', stack: false })
).toBe(false);
});
it('returns true for error like object', () => {
expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(true);
});
it('returns true for new Error', () => {
expect(isErrorLike(new Error('Error'))).toBe(true);
});
});
describe('isDuplicateTransactionError', () => {
it('returns true for a TransactionError with a transaction code of DUPLICATE_TXID', () => {
const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID';
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
true
);
});
it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => {
const mockDuplicateTransactionError = mock<TransactionError>();
mockDuplicateTransactionError.transactionCode = 'MVCC_READ_CONFLICT';
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
false
);
});
it('returns true for an error when all endorsement details are duplicate transaction found', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
true
);
});
it('returns true for an error when at least one endorsement details are duplicate transaction found', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'mock endorsement details',
},
{
details: 'mock endorsement details',
},
],
},
],
};
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
true
);
});
it('returns false for an error without duplicate transaction endorsement details', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'mock endorsement details',
},
{
details: 'mock endorsement details',
},
],
},
],
};
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
false
);
});
it('returns false for an error without endorsement details', () => {
const mockDuplicateTransactionError = {
errors: [
{
rejections: [
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(isDuplicateTransactionError(mockDuplicateTransactionError)).toBe(
false
);
});
it('returns false for a basic Error object without endorsement details', () => {
expect(
isDuplicateTransactionError(new Error('duplicate transaction found'))
).toBe(false);
});
it('returns false for an undefined error', () => {
expect(isDuplicateTransactionError(undefined)).toBe(false);
});
it('returns false for a null error', () => {
expect(isDuplicateTransactionError(null)).toBe(false);
});
});
describe('getRetryAction', () => {
it('returns RetryAction.None for duplicate transaction errors', () => {
const mockDuplicateTransactionError = {
errors: [
{
endorsements: [
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
{
details: 'duplicate transaction found',
},
],
},
],
};
expect(getRetryAction(mockDuplicateTransactionError)).toBe(
RetryAction.None
);
});
it('returns RetryAction.None for a TransactionNotFoundError', () => {
const mockTransactionNotFoundError = new TransactionNotFoundError(
'Failed to get transaction with id txn, error Entry not found in index',
'txn1'
);
expect(getRetryAction(mockTransactionNotFoundError)).toBe(
RetryAction.None
);
});
it('returns RetryAction.None for an AssetExistsError', () => {
const mockAssetExistsError = new AssetExistsError(
'The asset MOCK_ASSET already exists',
'txn1'
);
expect(getRetryAction(mockAssetExistsError)).toBe(RetryAction.None);
});
it('returns RetryAction.None for an AssetNotFoundError', () => {
const mockAssetNotFoundError = new AssetNotFoundError(
'the asset MOCK_ASSET does not exist',
'txn1'
);
expect(getRetryAction(mockAssetNotFoundError)).toBe(RetryAction.None);
});
it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => {
const mockTimeoutError = new TimeoutError('MOCK TIMEOUT ERROR');
expect(getRetryAction(mockTimeoutError)).toBe(
RetryAction.WithExistingTransactionId
);
});
it('returns RetryAction.WithNewTransactionId for an MVCC_READ_CONFLICT TransactionError', () => {
const mockTransactionError = mock<TransactionError>();
mockTransactionError.transactionCode = 'MVCC_READ_CONFLICT';
expect(getRetryAction(mockTransactionError)).toBe(
RetryAction.WithNewTransactionId
);
});
it('returns RetryAction.WithNewTransactionId for an Error', () => {
const mockError = new Error('MOCK ERROR');
expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId);
});
it('returns RetryAction.WithNewTransactionId for a string error', () => {
const mockError = 'MOCK ERROR';
expect(getRetryAction(mockError)).toBe(RetryAction.WithNewTransactionId);
});
});
describe('handleError', () => {
it.each([
'the asset GOCHAINCODE already exists',
'Asset JAVACHAINCODE already exists',
'The asset JSCHAINCODE already exists',
])(
'returns a AssetExistsError for errors with an asset already exists message: %s',
(msg) => {
expect(handleError('txn1', new Error(msg))).toStrictEqual(
new AssetExistsError(msg, 'txn1')
); );
}
);
it.each([ it.each([
'the asset GOCHAINCODE does not exist', 'the asset GOCHAINCODE does not exist',
'Asset JAVACHAINCODE does not exist', 'Asset JAVACHAINCODE does not exist',
'The asset JSCHAINCODE does not exist', 'The asset JSCHAINCODE does not exist',
])( ])(
'returns a AssetNotFoundError for errors with an asset does not exist message: %s', 'returns a AssetNotFoundError for errors with an asset does not exist message: %s',
(msg) => { (msg) => {
expect(handleError('txn1', new Error(msg))).toStrictEqual( expect(handleError('txn1', new Error(msg))).toStrictEqual(
new AssetNotFoundError(msg, 'txn1') new AssetNotFoundError(msg, 'txn1')
);
}
); );
}
);
it.each([ it.each([
'Failed to get transaction with id txn, error Entry not found in index', 'Failed to get transaction with id txn, error Entry not found in index',
'Failed to get transaction with id txn, error no such transaction ID [txn] in index', 'Failed to get transaction with id txn, error no such transaction ID [txn] in index',
])( ])(
'returns a TransactionNotFoundError for errors with a transaction not found message: %s', 'returns a TransactionNotFoundError for errors with a transaction not found message: %s',
(msg) => { (msg) => {
expect(handleError('txn1', new Error(msg))).toStrictEqual( expect(handleError('txn1', new Error(msg))).toStrictEqual(
new TransactionNotFoundError(msg, 'txn1') new TransactionNotFoundError(msg, 'txn1')
);
}
); );
}
);
it('returns the original error for errors with other messages', () => { it('returns the original error for errors with other messages', () => {
expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual( expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual(
new Error('MOCK ERROR') new Error('MOCK ERROR')
); );
}); });
it('returns the original error for errors of other types', () => { it('returns the original error for errors of other types', () => {
expect(handleError('txn1', 42)).toEqual(42); expect(handleError('txn1', 42)).toEqual(42);
});
}); });
});
}); });

View file

@ -14,15 +14,15 @@ import { logger } from './logger';
* These errors will not be retried. * These errors will not be retried.
*/ */
export class ContractError extends Error { export class ContractError extends Error {
transactionId: string; transactionId: string;
constructor(message: string, transactionId: string) { constructor(message: string, transactionId: string) {
super(message); super(message);
Object.setPrototypeOf(this, ContractError.prototype); Object.setPrototypeOf(this, ContractError.prototype);
this.name = 'TransactionError'; this.name = 'TransactionError';
this.transactionId = transactionId; this.transactionId = transactionId;
} }
} }
/** /**
@ -30,12 +30,12 @@ export class ContractError extends Error {
* evaluated is not implemented in a smart contract. * evaluated is not implemented in a smart contract.
*/ */
export class TransactionNotFoundError extends ContractError { export class TransactionNotFoundError extends ContractError {
constructor(message: string, transactionId: string) { constructor(message: string, transactionId: string) {
super(message, transactionId); super(message, transactionId);
Object.setPrototypeOf(this, TransactionNotFoundError.prototype); Object.setPrototypeOf(this, TransactionNotFoundError.prototype);
this.name = 'TransactionNotFoundError'; this.name = 'TransactionNotFoundError';
} }
} }
/** /**
@ -43,12 +43,12 @@ export class TransactionNotFoundError extends ContractError {
* implementation when an asset already exists. * implementation when an asset already exists.
*/ */
export class AssetExistsError extends ContractError { export class AssetExistsError extends ContractError {
constructor(message: string, transactionId: string) { constructor(message: string, transactionId: string) {
super(message, transactionId); super(message, transactionId);
Object.setPrototypeOf(this, AssetExistsError.prototype); Object.setPrototypeOf(this, AssetExistsError.prototype);
this.name = 'AssetExistsError'; this.name = 'AssetExistsError';
} }
} }
/** /**
@ -56,35 +56,35 @@ export class AssetExistsError extends ContractError {
* implementation when an asset does not exist. * implementation when an asset does not exist.
*/ */
export class AssetNotFoundError extends ContractError { export class AssetNotFoundError extends ContractError {
constructor(message: string, transactionId: string) { constructor(message: string, transactionId: string) {
super(message, transactionId); super(message, transactionId);
Object.setPrototypeOf(this, AssetNotFoundError.prototype); Object.setPrototypeOf(this, AssetNotFoundError.prototype);
this.name = 'AssetNotFoundError'; this.name = 'AssetNotFoundError';
} }
} }
/** /**
* Enumeration of possible retry actions. * Enumeration of possible retry actions.
*/ */
export enum RetryAction { export enum RetryAction {
/** /**
* Transactions should be retried using the same transaction ID to protect * Transactions should be retried using the same transaction ID to protect
* against duplicate transactions being committed if a timeout error occurs * against duplicate transactions being committed if a timeout error occurs
*/ */
WithExistingTransactionId, WithExistingTransactionId,
/** /**
* Transactions which could not be committed due to other errors require a * Transactions which could not be committed due to other errors require a
* new transaction ID when retrying * new transaction ID when retrying
*/ */
WithNewTransactionId, WithNewTransactionId,
/** /**
* Transactions that failed due to a duplicate transaction error, or errors * Transactions that failed due to a duplicate transaction error, or errors
* from the smart contract, should not be retried * from the smart contract, should not be retried
*/ */
None, None,
} }
/** /**
@ -103,27 +103,27 @@ export enum RetryAction {
* - EXPIRED_CHAINCODE * - EXPIRED_CHAINCODE
*/ */
export const getRetryAction = (err: unknown): RetryAction => { export const getRetryAction = (err: unknown): RetryAction => {
if (isDuplicateTransactionError(err) || err instanceof ContractError) { if (isDuplicateTransactionError(err) || err instanceof ContractError) {
return RetryAction.None; return RetryAction.None;
} else if (err instanceof TimeoutError) { } else if (err instanceof TimeoutError) {
return RetryAction.WithExistingTransactionId; return RetryAction.WithExistingTransactionId;
} }
return RetryAction.WithNewTransactionId; return RetryAction.WithNewTransactionId;
}; };
/** /**
* Type guard to make catching unknown errors easier * Type guard to make catching unknown errors easier
*/ */
export const isErrorLike = (err: unknown): err is Error => { export const isErrorLike = (err: unknown): err is Error => {
return ( return (
err != undefined && err != undefined &&
err != null && err != null &&
typeof (err as Error).name === 'string' && typeof (err as Error).name === 'string' &&
typeof (err as Error).message === 'string' && typeof (err as Error).message === 'string' &&
((err as Error).stack === undefined || ((err as Error).stack === undefined ||
typeof (err as Error).stack === 'string') typeof (err as Error).stack === 'string')
); );
}; };
/** /**
@ -132,31 +132,31 @@ export const isErrorLike = (err: unknown): err is Error => {
* This is ...painful. * This is ...painful.
*/ */
export const isDuplicateTransactionError = (err: unknown): boolean => { export const isDuplicateTransactionError = (err: unknown): boolean => {
logger.debug({ err }, 'Checking for duplicate transaction error'); logger.debug({ err }, 'Checking for duplicate transaction error');
if (err === undefined || err === null) return false; if (err === undefined || err === null) return false;
let isDuplicate; let isDuplicate;
if (typeof (err as TransactionError).transactionCode === 'string') { if (typeof (err as TransactionError).transactionCode === 'string') {
// Checking whether a commit failure is caused by a duplicate transaction // Checking whether a commit failure is caused by a duplicate transaction
// is straightforward because the transaction code should be available // is straightforward because the transaction code should be available
isDuplicate = isDuplicate =
(err as TransactionError).transactionCode === 'DUPLICATE_TXID'; (err as TransactionError).transactionCode === 'DUPLICATE_TXID';
} else { } else {
// Checking whether an endorsement failure is caused by a duplicate // Checking whether an endorsement failure is caused by a duplicate
// transaction is only possible by processing error strings, which is not ideal. // transaction is only possible by processing error strings, which is not ideal.
const endorsementError = err as { const endorsementError = err as {
errors: { endorsements: { details: string }[] }[]; errors: { endorsements: { details: string }[] }[];
}; };
isDuplicate = endorsementError?.errors?.some((err) => isDuplicate = endorsementError?.errors?.some((err) =>
err?.endorsements?.some((endorsement) => err?.endorsements?.some((endorsement) =>
endorsement?.details?.startsWith('duplicate transaction found') endorsement?.details?.startsWith('duplicate transaction found')
) )
); );
} }
return isDuplicate === true; return isDuplicate === true;
}; };
/** /**
@ -168,18 +168,18 @@ export const isDuplicateTransactionError = (err: unknown): boolean => {
* - "Asset %s already exists" * - "Asset %s already exists"
*/ */
const matchAssetAlreadyExistsMessage = (message: string): string | null => { const matchAssetAlreadyExistsMessage = (message: string): string | null => {
const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g; const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g;
const assetAlreadyExistsMatch = message.match(assetAlreadyExistsRegex); const assetAlreadyExistsMatch = message.match(assetAlreadyExistsRegex);
logger.debug( logger.debug(
{ message: message, result: assetAlreadyExistsMatch }, { message: message, result: assetAlreadyExistsMatch },
'Checking for asset already exists message' 'Checking for asset already exists message'
); );
if (assetAlreadyExistsMatch !== null) { if (assetAlreadyExistsMatch !== null) {
return assetAlreadyExistsMatch[0]; return assetAlreadyExistsMatch[0];
} }
return null; return null;
}; };
/** /**
@ -191,18 +191,18 @@ const matchAssetAlreadyExistsMessage = (message: string): string | null => {
* - "Asset %s does not exist" * - "Asset %s does not exist"
*/ */
const matchAssetDoesNotExistMessage = (message: string): string | null => { const matchAssetDoesNotExistMessage = (message: string): string | null => {
const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g; const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g;
const assetDoesNotExistMatch = message.match(assetDoesNotExistRegex); const assetDoesNotExistMatch = message.match(assetDoesNotExistRegex);
logger.debug( logger.debug(
{ message: message, result: assetDoesNotExistMatch }, { message: message, result: assetDoesNotExistMatch },
'Checking for asset does not exist message' 'Checking for asset does not exist message'
); );
if (assetDoesNotExistMatch !== null) { if (assetDoesNotExistMatch !== null) {
return assetDoesNotExistMatch[0]; return assetDoesNotExistMatch[0];
} }
return null; return null;
}; };
/** /**
@ -213,23 +213,23 @@ const matchAssetDoesNotExistMessage = (message: string): string | null => {
* - "Failed to get transaction with id %s, error no such transaction ID [%s] in index" * - "Failed to get transaction with id %s, error no such transaction ID [%s] in index"
*/ */
const matchTransactionDoesNotExistMessage = ( const matchTransactionDoesNotExistMessage = (
message: string message: string
): string | null => { ): string | null => {
const transactionDoesNotExistRegex = const transactionDoesNotExistRegex =
/Failed to get transaction with id [^,]*, error (?:(?:Entry not found)|(?:no such transaction ID \[[^\]]*\])) in index/g; /Failed to get transaction with id [^,]*, error (?:(?:Entry not found)|(?:no such transaction ID \[[^\]]*\])) in index/g;
const transactionDoesNotExistMatch = message.match( const transactionDoesNotExistMatch = message.match(
transactionDoesNotExistRegex transactionDoesNotExistRegex
); );
logger.debug( logger.debug(
{ message: message, result: transactionDoesNotExistMatch }, { message: message, result: transactionDoesNotExistMatch },
'Checking for transaction does not exist message' 'Checking for transaction does not exist message'
); );
if (transactionDoesNotExistMatch !== null) { if (transactionDoesNotExistMatch !== null) {
return transactionDoesNotExistMatch[0]; return transactionDoesNotExistMatch[0];
} }
return null; return null;
}; };
/** /**
@ -242,32 +242,38 @@ const matchTransactionDoesNotExistMessage = (
* Javascript implementations of the chaincode! * Javascript implementations of the chaincode!
*/ */
export const handleError = ( export const handleError = (
transactionId: string, transactionId: string,
err: unknown err: unknown
): Error | unknown => { ): Error | unknown => {
logger.debug({ transactionId: transactionId, err }, 'Processing error'); logger.debug({ transactionId: transactionId, err }, 'Processing error');
if (isErrorLike(err)) { if (isErrorLike(err)) {
const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(err.message); const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(
if (assetAlreadyExistsMatch !== null) { err.message
return new AssetExistsError(assetAlreadyExistsMatch, transactionId); );
if (assetAlreadyExistsMatch !== null) {
return new AssetExistsError(assetAlreadyExistsMatch, transactionId);
}
const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(
err.message
);
if (assetDoesNotExistMatch !== null) {
return new AssetNotFoundError(
assetDoesNotExistMatch,
transactionId
);
}
const transactionDoesNotExistMatch =
matchTransactionDoesNotExistMessage(err.message);
if (transactionDoesNotExistMatch !== null) {
return new TransactionNotFoundError(
transactionDoesNotExistMatch,
transactionId
);
}
} }
const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(err.message); return err;
if (assetDoesNotExistMatch !== null) {
return new AssetNotFoundError(assetDoesNotExistMatch, transactionId);
}
const transactionDoesNotExistMatch = matchTransactionDoesNotExistMessage(
err.message
);
if (transactionDoesNotExistMatch !== null) {
return new TransactionNotFoundError(
transactionDoesNotExistMatch,
transactionId
);
}
}
return err;
}; };

View file

@ -3,30 +3,30 @@
*/ */
import { import {
createGateway, createGateway,
createWallet, createWallet,
getContracts, getContracts,
getNetwork, getNetwork,
evatuateTransaction, evatuateTransaction,
submitTransaction, submitTransaction,
getBlockHeight, getBlockHeight,
getTransactionValidationCode, getTransactionValidationCode,
} from './fabric'; } from './fabric';
import * as config from './config'; import * as config from './config';
import { import {
AssetExistsError, AssetExistsError,
AssetNotFoundError, AssetNotFoundError,
TransactionNotFoundError, TransactionNotFoundError,
} from './errors'; } from './errors';
import { import {
Contract, Contract,
Gateway, Gateway,
GatewayOptions, GatewayOptions,
Network, Network,
Transaction, Transaction,
Wallet, Wallet,
} from 'fabric-network'; } from 'fabric-network';
import * as fabricProtos from 'fabric-protos'; import * as fabricProtos from 'fabric-protos';
@ -36,275 +36,279 @@ import Long from 'long';
jest.mock('./config'); jest.mock('./config');
jest.mock('fabric-network', () => { jest.mock('fabric-network', () => {
type FabricNetworkModule = jest.Mocked<typeof import('fabric-network')>; type FabricNetworkModule = jest.Mocked<typeof import('fabric-network')>;
const originalModule: FabricNetworkModule = const originalModule: FabricNetworkModule =
jest.requireActual('fabric-network'); jest.requireActual('fabric-network');
const mockModule: FabricNetworkModule = const mockModule: FabricNetworkModule =
jest.createMockFromModule('fabric-network'); jest.createMockFromModule('fabric-network');
return { return {
__esModule: true, __esModule: true,
...mockModule, ...mockModule,
Wallets: originalModule.Wallets, Wallets: originalModule.Wallets,
}; };
}); });
jest.mock('ioredis', () => require('ioredis-mock/jest')); jest.mock('ioredis', () => require('ioredis-mock/jest'));
describe('Fabric', () => { describe('Fabric', () => {
describe('createWallet', () => { describe('createWallet', () => {
it('creates a wallet containing identities for both orgs', async () => { it('creates a wallet containing identities for both orgs', async () => {
const wallet = await createWallet(); const wallet = await createWallet();
expect(await wallet.list()).toStrictEqual(['Org1MSP', 'Org2MSP']); expect(await wallet.list()).toStrictEqual(['Org1MSP', 'Org2MSP']);
}); });
});
describe('createGateway', () => {
it('creates a Gateway and connects using the provided arguments', async () => {
const connectionProfile = config.connectionProfileOrg1;
const identity = config.mspIdOrg1;
const mockWallet = mock<Wallet>();
const gateway = await createGateway(
connectionProfile,
identity,
mockWallet
);
expect(gateway.connect).toBeCalledWith(
connectionProfile,
expect.objectContaining<GatewayOptions>({
wallet: mockWallet,
identity,
discovery: expect.any(Object),
eventHandlerOptions: expect.any(Object),
queryHandlerOptions: expect.any(Object),
})
);
});
});
describe('getNetwork', () => {
it('gets a Network instance for the required channel from the Gateway', async () => {
const mockGateway = mock<Gateway>();
await getNetwork(mockGateway);
expect(mockGateway.getNetwork).toHaveBeenCalledWith(config.channelName);
});
});
describe('getContracts', () => {
it('gets the asset and qscc contracts from the network', async () => {
const mockBasicContract = mock<Contract>();
const mockSystemContract = mock<Contract>();
const mockNetwork = mock<Network>();
mockNetwork.getContract
.calledWith(config.chaincodeName)
.mockReturnValue(mockBasicContract);
mockNetwork.getContract
.calledWith('qscc')
.mockReturnValue(mockSystemContract);
const contracts = await getContracts(mockNetwork);
expect(contracts).toStrictEqual({
assetContract: mockBasicContract,
qsccContract: mockSystemContract,
});
});
});
describe('evatuateTransaction', () => {
const mockPayload = Buffer.from('MOCK PAYLOAD');
let mockTransaction: MockProxy<Transaction>;
let mockContract: MockProxy<Contract>;
beforeEach(() => {
mockTransaction = mock<Transaction>();
mockTransaction.evaluate.mockResolvedValue(mockPayload);
mockContract = mock<Contract>();
mockContract.createTransaction
.calledWith('txn')
.mockReturnValue(mockTransaction);
}); });
it('gets the result of evaluating a transaction', async () => { describe('createGateway', () => {
const result = await evatuateTransaction( it('creates a Gateway and connects using the provided arguments', async () => {
mockContract, const connectionProfile = config.connectionProfileOrg1;
'txn', const identity = config.mspIdOrg1;
'arga', const mockWallet = mock<Wallet>();
'argb'
); const gateway = await createGateway(
expect(result.toString()).toBe(mockPayload.toString()); connectionProfile,
identity,
mockWallet
);
expect(gateway.connect).toBeCalledWith(
connectionProfile,
expect.objectContaining<GatewayOptions>({
wallet: mockWallet,
identity,
discovery: expect.any(Object),
eventHandlerOptions: expect.any(Object),
queryHandlerOptions: expect.any(Object),
})
);
});
}); });
it('throws an AssetExistsError an asset already exists error occurs', async () => { describe('getNetwork', () => {
mockTransaction.evaluate.mockRejectedValue( it('gets a Network instance for the required channel from the Gateway', async () => {
new Error('The asset JSCHAINCODE already exists') const mockGateway = mock<Gateway>();
);
await expect(async () => { await getNetwork(mockGateway);
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(AssetExistsError); expect(mockGateway.getNetwork).toHaveBeenCalledWith(
config.channelName
);
});
}); });
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => { describe('getContracts', () => {
mockTransaction.evaluate.mockRejectedValue( it('gets the asset and qscc contracts from the network', async () => {
new Error('The asset JSCHAINCODE does not exist') const mockBasicContract = mock<Contract>();
); const mockSystemContract = mock<Contract>();
const mockNetwork = mock<Network>();
mockNetwork.getContract
.calledWith(config.chaincodeName)
.mockReturnValue(mockBasicContract);
mockNetwork.getContract
.calledWith('qscc')
.mockReturnValue(mockSystemContract);
await expect(async () => { const contracts = await getContracts(mockNetwork);
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(AssetNotFoundError); expect(contracts).toStrictEqual({
assetContract: mockBasicContract,
qsccContract: mockSystemContract,
});
});
}); });
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => { describe('evatuateTransaction', () => {
mockTransaction.evaluate.mockRejectedValue( const mockPayload = Buffer.from('MOCK PAYLOAD');
new Error( let mockTransaction: MockProxy<Transaction>;
'Failed to get transaction with id txn, error Entry not found in index' let mockContract: MockProxy<Contract>;
)
);
await expect(async () => { beforeEach(() => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb'); mockTransaction = mock<Transaction>();
}).rejects.toThrow(TransactionNotFoundError); mockTransaction.evaluate.mockResolvedValue(mockPayload);
mockContract = mock<Contract>();
mockContract.createTransaction
.calledWith('txn')
.mockReturnValue(mockTransaction);
});
it('gets the result of evaluating a transaction', async () => {
const result = await evatuateTransaction(
mockContract,
'txn',
'arga',
'argb'
);
expect(result.toString()).toBe(mockPayload.toString());
});
it('throws an AssetExistsError an asset already exists error occurs', async () => {
mockTransaction.evaluate.mockRejectedValue(
new Error('The asset JSCHAINCODE already exists')
);
await expect(async () => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(AssetExistsError);
});
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => {
mockTransaction.evaluate.mockRejectedValue(
new Error('The asset JSCHAINCODE does not exist')
);
await expect(async () => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(AssetNotFoundError);
});
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
mockTransaction.evaluate.mockRejectedValue(
new Error(
'Failed to get transaction with id txn, error Entry not found in index'
)
);
await expect(async () => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(TransactionNotFoundError);
});
it('throws an Error for other errors', async () => {
mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR'));
await expect(async () => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(Error);
});
}); });
it('throws an Error for other errors', async () => { describe('submitTransaction', () => {
mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR')); let mockTransaction: MockProxy<Transaction>;
await expect(async () => {
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
}).rejects.toThrow(Error);
});
});
describe('submitTransaction', () => { beforeEach(() => {
let mockTransaction: MockProxy<Transaction>; mockTransaction = mock<Transaction>();
});
beforeEach(() => { it('gets the result of submitting a transaction', async () => {
mockTransaction = mock<Transaction>(); const mockPayload = Buffer.from('MOCK PAYLOAD');
mockTransaction.submit.mockResolvedValue(mockPayload);
const result = await submitTransaction(
mockTransaction,
'txn',
'arga',
'argb'
);
expect(result.toString()).toBe(mockPayload.toString());
});
it('throws an AssetExistsError an asset already exists error occurs', async () => {
mockTransaction.submit.mockRejectedValue(
new Error('The asset JSCHAINCODE already exists')
);
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(AssetExistsError);
});
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => {
mockTransaction.submit.mockRejectedValue(
new Error('The asset JSCHAINCODE does not exist')
);
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(AssetNotFoundError);
});
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
mockTransaction.submit.mockRejectedValue(
new Error(
'Failed to get transaction with id txn, error Entry not found in index'
)
);
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(TransactionNotFoundError);
});
it('throws an Error for other errors', async () => {
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(Error);
});
}); });
it('gets the result of submitting a transaction', async () => { describe('getTransactionValidationCode', () => {
const mockPayload = Buffer.from('MOCK PAYLOAD'); it('gets the validation code from a processed transaction', async () => {
mockTransaction.submit.mockResolvedValue(mockPayload); const processedTransactionProto =
fabricProtos.protos.ProcessedTransaction.create();
processedTransactionProto.validationCode =
fabricProtos.protos.TxValidationCode.VALID;
const processedTransactionBuffer = Buffer.from(
fabricProtos.protos.ProcessedTransaction.encode(
processedTransactionProto
).finish()
);
const result = await submitTransaction( const mockTransaction = mock<Transaction>();
mockTransaction, mockTransaction.evaluate.mockResolvedValue(
'txn', processedTransactionBuffer
'arga', );
'argb' const mockContract = mock<Contract>();
); mockContract.createTransaction
expect(result.toString()).toBe(mockPayload.toString()); .calledWith('GetTransactionByID')
.mockReturnValue(mockTransaction);
expect(
await getTransactionValidationCode(mockContract, 'txn1')
).toBe('VALID');
});
}); });
it('throws an AssetExistsError an asset already exists error occurs', async () => { describe('getBlockHeight', () => {
mockTransaction.submit.mockRejectedValue( it('gets the current block height', async () => {
new Error('The asset JSCHAINCODE already exists') const mockBlockchainInfoProto =
); fabricProtos.common.BlockchainInfo.create();
mockBlockchainInfoProto.height = 42;
const mockBlockchainInfoBuffer = Buffer.from(
fabricProtos.common.BlockchainInfo.encode(
mockBlockchainInfoProto
).finish()
);
const mockContract = mock<Contract>();
mockContract.evaluateTransaction
.calledWith('GetChainInfo', 'mychannel')
.mockResolvedValue(mockBlockchainInfoBuffer);
await expect(async () => { const result = (await getBlockHeight(mockContract)) as Long;
await submitTransaction( expect(result.toInt()).toStrictEqual(42);
mockTransaction, });
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(AssetExistsError);
}); });
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => {
mockTransaction.submit.mockRejectedValue(
new Error('The asset JSCHAINCODE does not exist')
);
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(AssetNotFoundError);
});
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
mockTransaction.submit.mockRejectedValue(
new Error(
'Failed to get transaction with id txn, error Entry not found in index'
)
);
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(TransactionNotFoundError);
});
it('throws an Error for other errors', async () => {
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
await expect(async () => {
await submitTransaction(
mockTransaction,
'mspid',
'txn',
'arga',
'argb'
);
}).rejects.toThrow(Error);
});
});
describe('getTransactionValidationCode', () => {
it('gets the validation code from a processed transaction', async () => {
const processedTransactionProto =
fabricProtos.protos.ProcessedTransaction.create();
processedTransactionProto.validationCode =
fabricProtos.protos.TxValidationCode.VALID;
const processedTransactionBuffer = Buffer.from(
fabricProtos.protos.ProcessedTransaction.encode(
processedTransactionProto
).finish()
);
const mockTransaction = mock<Transaction>();
mockTransaction.evaluate.mockResolvedValue(processedTransactionBuffer);
const mockContract = mock<Contract>();
mockContract.createTransaction
.calledWith('GetTransactionByID')
.mockReturnValue(mockTransaction);
expect(await getTransactionValidationCode(mockContract, 'txn1')).toBe(
'VALID'
);
});
});
describe('getBlockHeight', () => {
it('gets the current block height', async () => {
const mockBlockchainInfoProto =
fabricProtos.common.BlockchainInfo.create();
mockBlockchainInfoProto.height = 42;
const mockBlockchainInfoBuffer = Buffer.from(
fabricProtos.common.BlockchainInfo.encode(
mockBlockchainInfoProto
).finish()
);
const mockContract = mock<Contract>();
mockContract.evaluateTransaction
.calledWith('GetChainInfo', 'mychannel')
.mockResolvedValue(mockBlockchainInfoBuffer);
const result = (await getBlockHeight(mockContract)) as Long;
expect(result.toInt()).toStrictEqual(42);
});
});
}); });

View file

@ -3,15 +3,15 @@
*/ */
import { import {
Contract, Contract,
DefaultEventHandlerStrategies, DefaultEventHandlerStrategies,
DefaultQueryHandlerStrategies, DefaultQueryHandlerStrategies,
Gateway, Gateway,
GatewayOptions, GatewayOptions,
Network, Network,
Transaction, Transaction,
Wallet, Wallet,
Wallets, Wallets,
} from 'fabric-network'; } from 'fabric-network';
import * as protos from 'fabric-protos'; import * as protos from 'fabric-protos';
import Long from 'long'; import Long from 'long';
@ -29,31 +29,31 @@ import { logger } from './logger';
* or it could use credentials supplied in the REST requests * or it could use credentials supplied in the REST requests
*/ */
export const createWallet = async (): Promise<Wallet> => { export const createWallet = async (): Promise<Wallet> => {
const wallet = await Wallets.newInMemoryWallet(); const wallet = await Wallets.newInMemoryWallet();
const org1Identity = { const org1Identity = {
credentials: { credentials: {
certificate: config.certificateOrg1, certificate: config.certificateOrg1,
privateKey: config.privateKeyOrg1, privateKey: config.privateKeyOrg1,
}, },
mspId: config.mspIdOrg1, mspId: config.mspIdOrg1,
type: 'X.509', type: 'X.509',
}; };
await wallet.put(config.mspIdOrg1, org1Identity); await wallet.put(config.mspIdOrg1, org1Identity);
const org2Identity = { const org2Identity = {
credentials: { credentials: {
certificate: config.certificateOrg2, certificate: config.certificateOrg2,
privateKey: config.privateKeyOrg2, privateKey: config.privateKeyOrg2,
}, },
mspId: config.mspIdOrg2, mspId: config.mspIdOrg2,
type: 'X.509', type: 'X.509',
}; };
await wallet.put(config.mspIdOrg2, org2Identity); await wallet.put(config.mspIdOrg2, org2Identity);
return wallet; return wallet;
}; };
/** /**
@ -62,32 +62,33 @@ export const createWallet = async (): Promise<Wallet> => {
* Gateway instances can and should be reused rather than connecting to submit every transaction * Gateway instances can and should be reused rather than connecting to submit every transaction
*/ */
export const createGateway = async ( export const createGateway = async (
connectionProfile: Record<string, unknown>, connectionProfile: Record<string, unknown>,
identity: string, identity: string,
wallet: Wallet wallet: Wallet
): Promise<Gateway> => { ): Promise<Gateway> => {
logger.debug({ connectionProfile, identity }, 'Configuring gateway'); logger.debug({ connectionProfile, identity }, 'Configuring gateway');
const gateway = new Gateway(); const gateway = new Gateway();
const options: GatewayOptions = { const options: GatewayOptions = {
wallet, wallet,
identity, identity,
discovery: { enabled: true, asLocalhost: config.asLocalhost }, discovery: { enabled: true, asLocalhost: config.asLocalhost },
eventHandlerOptions: { eventHandlerOptions: {
commitTimeout: config.commitTimeout, commitTimeout: config.commitTimeout,
endorseTimeout: config.endorseTimeout, endorseTimeout: config.endorseTimeout,
strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX, strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX,
}, },
queryHandlerOptions: { queryHandlerOptions: {
timeout: config.queryTimeout, timeout: config.queryTimeout,
strategy: DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN, strategy:
}, DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN,
}; },
};
await gateway.connect(connectionProfile, options); await gateway.connect(connectionProfile, options);
return gateway; return gateway;
}; };
/** /**
@ -97,8 +98,8 @@ export const createGateway = async (
* start a block event listener * start a block event listener
*/ */
export const getNetwork = async (gateway: Gateway): Promise<Network> => { export const getNetwork = async (gateway: Gateway): Promise<Network> => {
const network = await gateway.getNetwork(config.channelName); const network = await gateway.getNetwork(config.channelName);
return network; return network;
}; };
/** /**
@ -107,79 +108,80 @@ export const getNetwork = async (gateway: Gateway): Promise<Network> => {
* The system contract is used for the liveness REST endpoint * The system contract is used for the liveness REST endpoint
*/ */
export const getContracts = async ( export const getContracts = async (
network: Network network: Network
): Promise<{ assetContract: Contract; qsccContract: Contract }> => { ): Promise<{ assetContract: Contract; qsccContract: Contract }> => {
const assetContract = network.getContract(config.chaincodeName); const assetContract = network.getContract(config.chaincodeName);
const qsccContract = network.getContract('qscc'); const qsccContract = network.getContract('qscc');
return { assetContract, qsccContract }; return { assetContract, qsccContract };
}; };
/** /**
* Evaluate a transaction and handle any errors * Evaluate a transaction and handle any errors
*/ */
export const evatuateTransaction = async ( export const evatuateTransaction = async (
contract: Contract, contract: Contract,
transactionName: string, transactionName: string,
...transactionArgs: string[] ...transactionArgs: string[]
): Promise<Buffer> => { ): Promise<Buffer> => {
const transaction = contract.createTransaction(transactionName); const transaction = contract.createTransaction(transactionName);
const transactionId = transaction.getTransactionId(); const transactionId = transaction.getTransactionId();
logger.trace({ transaction }, 'Evaluating transaction'); logger.trace({ transaction }, 'Evaluating transaction');
try { try {
const payload = await transaction.evaluate(...transactionArgs); const payload = await transaction.evaluate(...transactionArgs);
logger.trace( logger.trace(
{ transactionId: transactionId, payload: payload.toString() }, { transactionId: transactionId, payload: payload.toString() },
'Evaluate transaction response received' 'Evaluate transaction response received'
); );
return payload; return payload;
} catch (err) { } catch (err) {
throw handleError(transactionId, err); throw handleError(transactionId, err);
} }
}; };
/** /**
* Submit a transaction and handle any errors * Submit a transaction and handle any errors
*/ */
export const submitTransaction = async ( export const submitTransaction = async (
transaction: Transaction, transaction: Transaction,
...transactionArgs: string[] ...transactionArgs: string[]
): Promise<Buffer> => { ): Promise<Buffer> => {
logger.trace({ transaction }, 'Submitting transaction'); logger.trace({ transaction }, 'Submitting transaction');
const txnId = transaction.getTransactionId(); const txnId = transaction.getTransactionId();
try { try {
const payload = await transaction.submit(...transactionArgs); const payload = await transaction.submit(...transactionArgs);
logger.trace( logger.trace(
{ transactionId: txnId, payload: payload.toString() }, { transactionId: txnId, payload: payload.toString() },
'Submit transaction response received' 'Submit transaction response received'
); );
return payload; return payload;
} catch (err) { } catch (err) {
throw handleError(txnId, err); throw handleError(txnId, err);
} }
}; };
/** /**
* Get the validation code of the specified transaction * Get the validation code of the specified transaction
*/ */
export const getTransactionValidationCode = async ( export const getTransactionValidationCode = async (
qsccContract: Contract, qsccContract: Contract,
transactionId: string transactionId: string
): Promise<string> => { ): Promise<string> => {
const data = await evatuateTransaction( const data = await evatuateTransaction(
qsccContract, qsccContract,
'GetTransactionByID', 'GetTransactionByID',
config.channelName, config.channelName,
transactionId transactionId
); );
const processedTransaction = protos.protos.ProcessedTransaction.decode(data); const processedTransaction =
const validationCode = protos.protos.ProcessedTransaction.decode(data);
protos.protos.TxValidationCode[processedTransaction.validationCode]; const validationCode =
protos.protos.TxValidationCode[processedTransaction.validationCode];
logger.debug({ transactionId }, 'Validation code: %s', validationCode); logger.debug({ transactionId }, 'Validation code: %s', validationCode);
return validationCode; return validationCode;
}; };
/** /**
@ -189,15 +191,15 @@ export const getTransactionValidationCode = async (
* endpoint * endpoint
*/ */
export const getBlockHeight = async ( export const getBlockHeight = async (
qscc: Contract qscc: Contract
): Promise<number | Long> => { ): Promise<number | Long> => {
const data = await qscc.evaluateTransaction( const data = await qscc.evaluateTransaction(
'GetChainInfo', 'GetChainInfo',
config.channelName config.channelName
); );
const info = protos.common.BlockchainInfo.decode(data); const info = protos.common.BlockchainInfo.decode(data);
const blockHeight = info.height; const blockHeight = info.height;
logger.debug('Current block height: %d', blockHeight); logger.debug('Current block height: %d', blockHeight);
return blockHeight; return blockHeight;
}; };

View file

@ -20,36 +20,38 @@ export const healthRouter = express.Router();
*/ */
healthRouter.get('/ready', (_req, res: Response) => healthRouter.get('/ready', (_req, res: Response) =>
res.status(OK).json({ res.status(OK).json({
status: getReasonPhrase(OK), status: getReasonPhrase(OK),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
); );
healthRouter.get('/live', async (req: Request, res: Response) => { healthRouter.get('/live', async (req: Request, res: Response) => {
logger.debug(req.body, 'Liveness request received'); logger.debug(req.body, 'Liveness request received');
try { try {
const submitQueue = req.app.locals.jobq as Queue; const submitQueue = req.app.locals.jobq as Queue;
const qsccOrg1 = req.app.locals[config.mspIdOrg1]?.qsccContract as Contract; const qsccOrg1 = req.app.locals[config.mspIdOrg1]
const qsccOrg2 = req.app.locals[config.mspIdOrg2]?.qsccContract as Contract; ?.qsccContract as Contract;
const qsccOrg2 = req.app.locals[config.mspIdOrg2]
?.qsccContract as Contract;
await Promise.all([ await Promise.all([
getBlockHeight(qsccOrg1), getBlockHeight(qsccOrg1),
getBlockHeight(qsccOrg2), getBlockHeight(qsccOrg2),
getJobCounts(submitQueue), getJobCounts(submitQueue),
]); ]);
} catch (err) { } catch (err) {
logger.error({ err }, 'Error processing liveness request'); logger.error({ err }, 'Error processing liveness request');
return res.status(SERVICE_UNAVAILABLE).json({ return res.status(SERVICE_UNAVAILABLE).json({
status: getReasonPhrase(SERVICE_UNAVAILABLE), status: getReasonPhrase(SERVICE_UNAVAILABLE),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
});
}
return res.status(OK).json({
status: getReasonPhrase(OK),
timestamp: new Date().toISOString(),
}); });
}
return res.status(OK).json({
status: getReasonPhrase(OK),
timestamp: new Date().toISOString(),
});
}); });

View file

@ -8,15 +8,15 @@
import * as config from './config'; import * as config from './config';
import { import {
createGateway, createGateway,
createWallet, createWallet,
getContracts, getContracts,
getNetwork, getNetwork,
} from './fabric'; } from './fabric';
import { import {
initJobQueue, initJobQueue,
initJobQueueScheduler, initJobQueueScheduler,
initJobQueueWorker, initJobQueueWorker,
} from './jobs'; } from './jobs';
import { logger } from './logger'; import { logger } from './logger';
import { createServer } from './server'; import { createServer } from './server';
@ -28,70 +28,70 @@ let jobQueueWorker: Worker | undefined;
let jobQueueScheduler: QueueScheduler | undefined; let jobQueueScheduler: QueueScheduler | undefined;
async function main() { async function main() {
logger.info('Checking Redis config'); logger.info('Checking Redis config');
if (!(await isMaxmemoryPolicyNoeviction())) { if (!(await isMaxmemoryPolicyNoeviction())) {
throw new Error( throw new Error(
'Invalid redis configuration: redis instance must have the setting maxmemory-policy=noeviction' 'Invalid redis configuration: redis instance must have the setting maxmemory-policy=noeviction'
);
}
logger.info('Creating REST server');
const app = await createServer();
logger.info('Connecting to Fabric network with org1 mspid');
const wallet = await createWallet();
const gatewayOrg1 = await createGateway(
config.connectionProfileOrg1,
config.mspIdOrg1,
wallet
); );
} const networkOrg1 = await getNetwork(gatewayOrg1);
const contractsOrg1 = await getContracts(networkOrg1);
logger.info('Creating REST server'); app.locals[config.mspIdOrg1] = contractsOrg1;
const app = await createServer();
logger.info('Connecting to Fabric network with org1 mspid'); logger.info('Connecting to Fabric network with org2 mspid');
const wallet = await createWallet(); const gatewayOrg2 = await createGateway(
config.connectionProfileOrg2,
config.mspIdOrg2,
wallet
);
const networkOrg2 = await getNetwork(gatewayOrg2);
const contractsOrg2 = await getContracts(networkOrg2);
const gatewayOrg1 = await createGateway( app.locals[config.mspIdOrg2] = contractsOrg2;
config.connectionProfileOrg1,
config.mspIdOrg1,
wallet
);
const networkOrg1 = await getNetwork(gatewayOrg1);
const contractsOrg1 = await getContracts(networkOrg1);
app.locals[config.mspIdOrg1] = contractsOrg1; logger.info('Initialising submit job queue');
jobQueue = initJobQueue();
jobQueueWorker = initJobQueueWorker(app);
if (config.submitJobQueueScheduler === true) {
logger.info('Initialising submit job queue scheduler');
jobQueueScheduler = initJobQueueScheduler();
}
app.locals.jobq = jobQueue;
logger.info('Connecting to Fabric network with org2 mspid'); logger.info('Starting REST server');
const gatewayOrg2 = await createGateway( app.listen(config.port, () => {
config.connectionProfileOrg2, logger.info('REST server started on port: %d', config.port);
config.mspIdOrg2, });
wallet
);
const networkOrg2 = await getNetwork(gatewayOrg2);
const contractsOrg2 = await getContracts(networkOrg2);
app.locals[config.mspIdOrg2] = contractsOrg2;
logger.info('Initialising submit job queue');
jobQueue = initJobQueue();
jobQueueWorker = initJobQueueWorker(app);
if (config.submitJobQueueScheduler === true) {
logger.info('Initialising submit job queue scheduler');
jobQueueScheduler = initJobQueueScheduler();
}
app.locals.jobq = jobQueue;
logger.info('Starting REST server');
app.listen(config.port, () => {
logger.info('REST server started on port: %d', config.port);
});
} }
main().catch(async (err) => { main().catch(async (err) => {
logger.error({ err }, 'Unxepected error'); logger.error({ err }, 'Unxepected error');
if (jobQueueScheduler != undefined) { if (jobQueueScheduler != undefined) {
logger.debug('Closing job queue scheduler'); logger.debug('Closing job queue scheduler');
await jobQueueScheduler.close(); await jobQueueScheduler.close();
} }
if (jobQueueWorker != undefined) { if (jobQueueWorker != undefined) {
logger.debug('Closing job queue worker'); logger.debug('Closing job queue worker');
await jobQueueWorker.close(); await jobQueueWorker.close();
} }
if (jobQueue != undefined) { if (jobQueue != undefined) {
logger.debug('Closing job queue'); logger.debug('Closing job queue');
await jobQueue.close(); await jobQueue.close();
} }
}); });

View file

@ -13,28 +13,32 @@ const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
export const jobsRouter = express.Router(); export const jobsRouter = express.Router();
jobsRouter.get('/:jobId', async (req: Request, res: Response) => { jobsRouter.get('/:jobId', async (req: Request, res: Response) => {
const jobId = req.params.jobId; const jobId = req.params.jobId;
logger.debug('Read request received for job ID %s', jobId); logger.debug('Read request received for job ID %s', jobId);
try { try {
const submitQueue = req.app.locals.jobq as Queue; const submitQueue = req.app.locals.jobq as Queue;
const jobSummary = await getJobSummary(submitQueue, jobId); const jobSummary = await getJobSummary(submitQueue, jobId);
return res.status(OK).json(jobSummary); return res.status(OK).json(jobSummary);
} catch (err) { } catch (err) {
logger.error({ err }, 'Error processing read request for job ID %s', jobId); logger.error(
{ err },
'Error processing read request for job ID %s',
jobId
);
if (err instanceof JobNotFoundError) { if (err instanceof JobNotFoundError) {
return res.status(NOT_FOUND).json({ return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND), status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
} }
return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(),
});
}
}); });

View file

@ -4,346 +4,348 @@
import { Job, Queue } from 'bullmq'; import { Job, Queue } from 'bullmq';
import { import {
addSubmitTransactionJob, addSubmitTransactionJob,
getJobCounts, getJobCounts,
getJobSummary, getJobSummary,
processSubmitTransactionJob, processSubmitTransactionJob,
JobNotFoundError, JobNotFoundError,
updateJobData, updateJobData,
} from './jobs'; } from './jobs';
import { Contract, Transaction } from 'fabric-network'; import { Contract, Transaction } from 'fabric-network';
import { mock, MockProxy } from 'jest-mock-extended'; import { mock, MockProxy } from 'jest-mock-extended';
import { Application } from 'express'; import { Application } from 'express';
describe('addSubmitTransactionJob', () => { describe('addSubmitTransactionJob', () => {
let mockJob: MockProxy<Job>; let mockJob: MockProxy<Job>;
let mockQueue: MockProxy<Queue>; let mockQueue: MockProxy<Queue>;
beforeEach(() => { beforeEach(() => {
mockJob = mock<Job>(); mockJob = mock<Job>();
mockQueue = mock<Queue>(); mockQueue = mock<Queue>();
mockQueue.add.mockResolvedValue(mockJob); mockQueue.add.mockResolvedValue(mockJob);
}); });
it('returns the new job ID', async () => { it('returns the new job ID', async () => {
mockJob.id = 'mockJobId'; mockJob.id = 'mockJobId';
const jobid = await addSubmitTransactionJob( const jobid = await addSubmitTransactionJob(
mockQueue, mockQueue,
'mockMspId', 'mockMspId',
'txn', 'txn',
'arg1', 'arg1',
'arg2' 'arg2'
); );
expect(jobid).toBe('mockJobId'); expect(jobid).toBe('mockJobId');
}); });
it('throws an error if there is no job ID', async () => { it('throws an error if there is no job ID', async () => {
mockJob.id = undefined; mockJob.id = undefined;
await expect(async () => { await expect(async () => {
await addSubmitTransactionJob( await addSubmitTransactionJob(
mockQueue, mockQueue,
'mockMspId', 'mockMspId',
'txn', 'txn',
'arg1', 'arg1',
'arg2' 'arg2'
); );
}).rejects.toThrowError('Submit transaction job ID not available'); }).rejects.toThrowError('Submit transaction job ID not available');
}); });
}); });
describe('getJobSummary', () => { describe('getJobSummary', () => {
let mockQueue: MockProxy<Queue>; let mockQueue: MockProxy<Queue>;
let mockJob: MockProxy<Job>;
beforeEach(() => {
mockQueue = mock<Queue>();
mockJob = mock<Job>();
});
it('throws a JobNotFoundError if the Job is undefined', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(undefined);
await expect(async () => {
await getJobSummary(mockQueue, '1');
}).rejects.toThrow(JobNotFoundError);
});
it('gets a job summary with transaction payload data', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.data = {
transactionIds: ['txn1'],
};
mockJob.returnvalue = {
transactionPayload: Buffer.from('MOCK PAYLOAD'),
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: ['txn1'],
transactionError: undefined,
transactionPayload: 'MOCK PAYLOAD',
});
});
it('gets a job summary with empty transaction payload data', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.data = {
transactionIds: ['txn1'],
};
mockJob.returnvalue = {
transactionPayload: Buffer.from(''),
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: ['txn1'],
transactionError: undefined,
transactionPayload: '',
});
});
it('gets a job summary with a transaction error', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.data = {
transactionIds: ['txn1'],
};
mockJob.returnvalue = {
transactionError: 'MOCK ERROR',
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: ['txn1'],
transactionError: 'MOCK ERROR',
transactionPayload: '',
});
});
it('gets a job summary when there is no return value', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.returnvalue = undefined;
mockJob.data = {
transactionIds: ['txn1'],
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: ['txn1'],
transactionError: undefined,
transactionPayload: undefined,
});
});
it('gets a job summary when there is no job data', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.data = undefined;
mockJob.returnvalue = {
transactionPayload: Buffer.from('MOCK PAYLOAD'),
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: [],
transactionError: undefined,
transactionPayload: 'MOCK PAYLOAD',
});
});
});
describe('updateJobData', () => {
let mockJob: MockProxy<Job>;
beforeEach(() => {
mockJob = mock<Job>();
mockJob.data = {
transactionIds: ['txn1'],
};
});
it('stores the serialized state in the job data if a transaction is specified', async () => {
const mockSavedState = Buffer.from('MOCK SAVED STATE');
const mockTransaction = mock<Transaction>();
mockTransaction.getTransactionId.mockReturnValue('txn2');
mockTransaction.serialize.mockReturnValue(mockSavedState);
await updateJobData(mockJob, mockTransaction);
expect(mockJob.update).toBeCalledTimes(1);
expect(mockJob.update).toBeCalledWith({
transactionIds: ['txn1', 'txn2'],
transactionState: mockSavedState,
});
});
it('removes the serialized state from the job data if a transaction is not specified', async () => {
await updateJobData(mockJob, undefined);
expect(mockJob.update).toBeCalledTimes(1);
expect(mockJob.update).toBeCalledWith({
transactionIds: ['txn1'],
transactionState: undefined,
});
});
});
describe('getJobCounts', () => {
it('gets job counts from the specified queue', async () => {
const mockQueue = mock<Queue>();
mockQueue.getJobCounts
.calledWith('active', 'completed', 'delayed', 'failed', 'waiting')
.mockResolvedValue({
active: 1,
completed: 2,
delayed: 3,
failed: 4,
waiting: 5,
});
expect(await getJobCounts(mockQueue)).toStrictEqual({
active: 1,
completed: 2,
delayed: 3,
failed: 4,
waiting: 5,
});
});
describe('processSubmitTransactionJob', () => {
const mockContracts = new Map<string, Contract>();
const mockPayload = Buffer.from('MOCK PAYLOAD');
const mockSavedState = Buffer.from('MOCK SAVED STATE');
let mockTransaction: MockProxy<Transaction>;
let mockContract: MockProxy<Contract>;
let mockApplication: MockProxy<Application>;
let mockJob: MockProxy<Job>; let mockJob: MockProxy<Job>;
beforeEach(() => { beforeEach(() => {
mockTransaction = mock<Transaction>(); mockQueue = mock<Queue>();
mockTransaction.getTransactionId.mockReturnValue('mockTransactionId'); mockJob = mock<Job>();
mockContract = mock<Contract>();
mockContract.createTransaction
.calledWith('txn')
.mockReturnValue(mockTransaction);
mockContract.deserializeTransaction
.calledWith(mockSavedState)
.mockReturnValue(mockTransaction);
mockContracts.set('mockMspid', mockContract);
mockApplication = mock<Application>();
mockApplication.locals.mockMspid = { assetContract: mockContract };
mockJob = mock<Job>();
}); });
it('gets job result with no error or payload if no contract is available for the required mspid', async () => { it('throws a JobNotFoundError if the Job is undefined', async () => {
mockJob.data = { mockQueue.getJob.calledWith('1').mockResolvedValue(undefined);
mspid: 'missingMspid',
};
const jobResult = await processSubmitTransactionJob( await expect(async () => {
mockApplication, await getJobSummary(mockQueue, '1');
mockJob }).rejects.toThrow(JobNotFoundError);
);
expect(jobResult).toStrictEqual({
transactionError: undefined,
transactionPayload: undefined,
});
}); });
it('gets a job result containing a payload if the transaction was successful first time', async () => { it('gets a job summary with transaction payload data', async () => {
mockJob.data = { mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mspid: 'mockMspid', mockJob.id = '1';
transactionName: 'txn', mockJob.data = {
transactionArgs: ['arg1', 'arg2'], transactionIds: ['txn1'],
}; };
mockTransaction.submit mockJob.returnvalue = {
.calledWith('arg1', 'arg2') transactionPayload: Buffer.from('MOCK PAYLOAD'),
.mockResolvedValue(mockPayload); };
const jobResult = await processSubmitTransactionJob( expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
mockApplication, jobId: '1',
mockJob transactionIds: ['txn1'],
); transactionError: undefined,
transactionPayload: 'MOCK PAYLOAD',
expect(jobResult).toStrictEqual({ });
transactionError: undefined,
transactionPayload: Buffer.from('MOCK PAYLOAD'),
});
}); });
it('gets a job result containing a payload if the transaction was successfully rerun using saved transaction state', async () => { it('gets a job summary with empty transaction payload data', async () => {
mockJob.data = { mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mspid: 'mockMspid', mockJob.id = '1';
transactionName: 'txn', mockJob.data = {
transactionArgs: ['arg1', 'arg2'], transactionIds: ['txn1'],
transactionState: mockSavedState, };
}; mockJob.returnvalue = {
mockTransaction.submit transactionPayload: Buffer.from(''),
.calledWith('arg1', 'arg2') };
.mockResolvedValue(mockPayload);
const jobResult = await processSubmitTransactionJob( expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
mockApplication, jobId: '1',
mockJob transactionIds: ['txn1'],
); transactionError: undefined,
transactionPayload: '',
expect(jobResult).toStrictEqual({ });
transactionError: undefined,
transactionPayload: Buffer.from('MOCK PAYLOAD'),
});
}); });
it('gets a job result containing an error message if the transaction fails but cannot be retried', async () => { it('gets a job summary with a transaction error', async () => {
mockJob.data = { mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mspid: 'mockMspid', mockJob.id = '1';
transactionName: 'txn', mockJob.data = {
transactionArgs: ['arg1', 'arg2'], transactionIds: ['txn1'],
transactionState: mockSavedState, };
}; mockJob.returnvalue = {
mockTransaction.submit transactionError: 'MOCK ERROR',
.calledWith('arg1', 'arg2') };
.mockRejectedValue(
new Error(
'Failed to get transaction with id txn, error Entry not found in index'
)
);
const jobResult = await processSubmitTransactionJob( expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
mockApplication, jobId: '1',
mockJob transactionIds: ['txn1'],
); transactionError: 'MOCK ERROR',
transactionPayload: '',
expect(jobResult).toStrictEqual({ });
transactionError:
'TransactionNotFoundError: Failed to get transaction with id txn, error Entry not found in index',
transactionPayload: undefined,
});
}); });
it('throws an error if the transaction fails but can be retried', async () => { it('gets a job summary when there is no return value', async () => {
mockJob.data = { mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mspid: 'mockMspid', mockJob.id = '1';
transactionName: 'txn', mockJob.returnvalue = undefined;
transactionArgs: ['arg1', 'arg2'], mockJob.data = {
transactionState: mockSavedState, transactionIds: ['txn1'],
}; };
mockTransaction.submit
.calledWith('arg1', 'arg2')
.mockRejectedValue(new Error('MOCK ERROR'));
await expect(async () => { expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
await processSubmitTransactionJob(mockApplication, mockJob); jobId: '1',
}).rejects.toThrow('MOCK ERROR'); transactionIds: ['txn1'],
transactionError: undefined,
transactionPayload: undefined,
});
});
it('gets a job summary when there is no job data', async () => {
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
mockJob.id = '1';
mockJob.data = undefined;
mockJob.returnvalue = {
transactionPayload: Buffer.from('MOCK PAYLOAD'),
};
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
jobId: '1',
transactionIds: [],
transactionError: undefined,
transactionPayload: 'MOCK PAYLOAD',
});
});
});
describe('updateJobData', () => {
let mockJob: MockProxy<Job>;
beforeEach(() => {
mockJob = mock<Job>();
mockJob.data = {
transactionIds: ['txn1'],
};
});
it('stores the serialized state in the job data if a transaction is specified', async () => {
const mockSavedState = Buffer.from('MOCK SAVED STATE');
const mockTransaction = mock<Transaction>();
mockTransaction.getTransactionId.mockReturnValue('txn2');
mockTransaction.serialize.mockReturnValue(mockSavedState);
await updateJobData(mockJob, mockTransaction);
expect(mockJob.update).toBeCalledTimes(1);
expect(mockJob.update).toBeCalledWith({
transactionIds: ['txn1', 'txn2'],
transactionState: mockSavedState,
});
});
it('removes the serialized state from the job data if a transaction is not specified', async () => {
await updateJobData(mockJob, undefined);
expect(mockJob.update).toBeCalledTimes(1);
expect(mockJob.update).toBeCalledWith({
transactionIds: ['txn1'],
transactionState: undefined,
});
});
});
describe('getJobCounts', () => {
it('gets job counts from the specified queue', async () => {
const mockQueue = mock<Queue>();
mockQueue.getJobCounts
.calledWith('active', 'completed', 'delayed', 'failed', 'waiting')
.mockResolvedValue({
active: 1,
completed: 2,
delayed: 3,
failed: 4,
waiting: 5,
});
expect(await getJobCounts(mockQueue)).toStrictEqual({
active: 1,
completed: 2,
delayed: 3,
failed: 4,
waiting: 5,
});
});
describe('processSubmitTransactionJob', () => {
const mockContracts = new Map<string, Contract>();
const mockPayload = Buffer.from('MOCK PAYLOAD');
const mockSavedState = Buffer.from('MOCK SAVED STATE');
let mockTransaction: MockProxy<Transaction>;
let mockContract: MockProxy<Contract>;
let mockApplication: MockProxy<Application>;
let mockJob: MockProxy<Job>;
beforeEach(() => {
mockTransaction = mock<Transaction>();
mockTransaction.getTransactionId.mockReturnValue(
'mockTransactionId'
);
mockContract = mock<Contract>();
mockContract.createTransaction
.calledWith('txn')
.mockReturnValue(mockTransaction);
mockContract.deserializeTransaction
.calledWith(mockSavedState)
.mockReturnValue(mockTransaction);
mockContracts.set('mockMspid', mockContract);
mockApplication = mock<Application>();
mockApplication.locals.mockMspid = { assetContract: mockContract };
mockJob = mock<Job>();
});
it('gets job result with no error or payload if no contract is available for the required mspid', async () => {
mockJob.data = {
mspid: 'missingMspid',
};
const jobResult = await processSubmitTransactionJob(
mockApplication,
mockJob
);
expect(jobResult).toStrictEqual({
transactionError: undefined,
transactionPayload: undefined,
});
});
it('gets a job result containing a payload if the transaction was successful first time', async () => {
mockJob.data = {
mspid: 'mockMspid',
transactionName: 'txn',
transactionArgs: ['arg1', 'arg2'],
};
mockTransaction.submit
.calledWith('arg1', 'arg2')
.mockResolvedValue(mockPayload);
const jobResult = await processSubmitTransactionJob(
mockApplication,
mockJob
);
expect(jobResult).toStrictEqual({
transactionError: undefined,
transactionPayload: Buffer.from('MOCK PAYLOAD'),
});
});
it('gets a job result containing a payload if the transaction was successfully rerun using saved transaction state', async () => {
mockJob.data = {
mspid: 'mockMspid',
transactionName: 'txn',
transactionArgs: ['arg1', 'arg2'],
transactionState: mockSavedState,
};
mockTransaction.submit
.calledWith('arg1', 'arg2')
.mockResolvedValue(mockPayload);
const jobResult = await processSubmitTransactionJob(
mockApplication,
mockJob
);
expect(jobResult).toStrictEqual({
transactionError: undefined,
transactionPayload: Buffer.from('MOCK PAYLOAD'),
});
});
it('gets a job result containing an error message if the transaction fails but cannot be retried', async () => {
mockJob.data = {
mspid: 'mockMspid',
transactionName: 'txn',
transactionArgs: ['arg1', 'arg2'],
transactionState: mockSavedState,
};
mockTransaction.submit
.calledWith('arg1', 'arg2')
.mockRejectedValue(
new Error(
'Failed to get transaction with id txn, error Entry not found in index'
)
);
const jobResult = await processSubmitTransactionJob(
mockApplication,
mockJob
);
expect(jobResult).toStrictEqual({
transactionError:
'TransactionNotFoundError: Failed to get transaction with id txn, error Entry not found in index',
transactionPayload: undefined,
});
});
it('throws an error if the transaction fails but can be retried', async () => {
mockJob.data = {
mspid: 'mockMspid',
transactionName: 'txn',
transactionArgs: ['arg1', 'arg2'],
transactionState: mockSavedState,
};
mockTransaction.submit
.calledWith('arg1', 'arg2')
.mockRejectedValue(new Error('MOCK ERROR'));
await expect(async () => {
await processSubmitTransactionJob(mockApplication, mockJob);
}).rejects.toThrow('MOCK ERROR');
});
}); });
});
}); });

View file

@ -14,62 +14,62 @@ import { submitTransaction } from './fabric';
import { logger } from './logger'; import { logger } from './logger';
export type JobData = { export type JobData = {
mspid: string; mspid: string;
transactionName: string; transactionName: string;
transactionArgs: string[]; transactionArgs: string[];
transactionState?: Buffer; transactionState?: Buffer;
transactionIds: string[]; transactionIds: string[];
}; };
export type JobResult = { export type JobResult = {
transactionPayload?: Buffer; transactionPayload?: Buffer;
transactionError?: string; transactionError?: string;
}; };
export type JobSummary = { export type JobSummary = {
jobId: string; jobId: string;
transactionIds: string[]; transactionIds: string[];
transactionPayload?: string; transactionPayload?: string;
transactionError?: string; transactionError?: string;
}; };
export class JobNotFoundError extends Error { export class JobNotFoundError extends Error {
jobId: string; jobId: string;
constructor(message: string, jobId: string) { constructor(message: string, jobId: string) {
super(message); super(message);
Object.setPrototypeOf(this, JobNotFoundError.prototype); Object.setPrototypeOf(this, JobNotFoundError.prototype);
this.name = 'JobNotFoundError'; this.name = 'JobNotFoundError';
this.jobId = jobId; this.jobId = jobId;
} }
} }
const connection: ConnectionOptions = { const connection: ConnectionOptions = {
port: config.redisPort, port: config.redisPort,
host: config.redisHost, host: config.redisHost,
username: config.redisUsername, username: config.redisUsername,
password: config.redisPassword, password: config.redisPassword,
}; };
/** /**
* Set up the queue for submit jobs * Set up the queue for submit jobs
*/ */
export const initJobQueue = (): Queue => { export const initJobQueue = (): Queue => {
const submitQueue = new Queue(config.JOB_QUEUE_NAME, { const submitQueue = new Queue(config.JOB_QUEUE_NAME, {
connection, connection,
defaultJobOptions: { defaultJobOptions: {
attempts: config.submitJobAttempts, attempts: config.submitJobAttempts,
backoff: { backoff: {
type: config.submitJobBackoffType, type: config.submitJobBackoffType,
delay: config.submitJobBackoffDelay, delay: config.submitJobBackoffDelay,
}, },
removeOnComplete: config.maxCompletedSubmitJobs, removeOnComplete: config.maxCompletedSubmitJobs,
removeOnFail: config.maxFailedSubmitJobs, removeOnFail: config.maxFailedSubmitJobs,
}, },
}); });
return submitQueue; return submitQueue;
}; };
/** /**
@ -77,31 +77,31 @@ export const initJobQueue = (): Queue => {
* processSubmitTransactionJob function below * processSubmitTransactionJob function below
*/ */
export const initJobQueueWorker = (app: Application): Worker => { export const initJobQueueWorker = (app: Application): Worker => {
const worker = new Worker<JobData, JobResult>( const worker = new Worker<JobData, JobResult>(
config.JOB_QUEUE_NAME, config.JOB_QUEUE_NAME,
async (job): Promise<JobResult> => { async (job): Promise<JobResult> => {
return await processSubmitTransactionJob(app, job); return await processSubmitTransactionJob(app, job);
}, },
{ connection, concurrency: config.submitJobConcurrency } { connection, concurrency: config.submitJobConcurrency }
); );
worker.on('failed', (job) => { worker.on('failed', (job) => {
logger.warn({ job }, 'Job failed'); logger.warn({ job }, 'Job failed');
});
// Important: need to handle this error otherwise worker may stop
// processing jobs
worker.on('error', (err) => {
logger.error({ err }, 'Worker error');
});
if (logger.isLevelEnabled('debug')) {
worker.on('completed', (job) => {
logger.debug({ job }, 'Job completed');
}); });
}
return worker; // Important: need to handle this error otherwise worker may stop
// processing jobs
worker.on('error', (err) => {
logger.error({ err }, 'Worker error');
});
if (logger.isLevelEnabled('debug')) {
worker.on('completed', (job) => {
logger.debug({ job }, 'Job completed');
});
}
return worker;
}; };
/** /**
@ -110,103 +110,103 @@ export const initJobQueueWorker = (app: Application): Worker => {
* The job will be retried if this function throws an error * The job will be retried if this function throws an error
*/ */
export const processSubmitTransactionJob = async ( export const processSubmitTransactionJob = async (
app: Application, app: Application,
job: Job<JobData, JobResult> job: Job<JobData, JobResult>
): Promise<JobResult> => { ): Promise<JobResult> => {
logger.debug({ jobId: job.id, jobName: job.name }, 'Processing job'); logger.debug({ jobId: job.id, jobName: job.name }, 'Processing job');
const contract = app.locals[job.data.mspid]?.assetContract as Contract; const contract = app.locals[job.data.mspid]?.assetContract as Contract;
if (contract === undefined) { if (contract === undefined) {
logger.error( logger.error(
{ jobId: job.id, jobName: job.name }, { jobId: job.id, jobName: job.name },
'Contract not found for MSP ID %s', 'Contract not found for MSP ID %s',
job.data.mspid job.data.mspid
); );
// Retrying will never work without a contract, so give up with an // Retrying will never work without a contract, so give up with an
// empty job result // empty job result
return { return {
transactionError: undefined, transactionError: undefined,
transactionPayload: undefined, transactionPayload: undefined,
}; };
}
const args = job.data.transactionArgs;
let transaction: Transaction;
if (job.data.transactionState) {
const savedState = job.data.transactionState;
logger.debug(
{
jobId: job.id,
jobName: job.name,
savedState,
},
'Reusing previously saved transaction state'
);
transaction = contract.deserializeTransaction(savedState);
} else {
logger.debug(
{
jobId: job.id,
jobName: job.name,
},
'Using new transaction'
);
transaction = contract.createTransaction(job.data.transactionName);
await updateJobData(job, transaction);
}
logger.debug(
{
jobId: job.id,
jobName: job.name,
transactionId: transaction.getTransactionId(),
},
'Submitting transaction'
);
try {
const payload = await submitTransaction(transaction, ...args);
return {
transactionError: undefined,
transactionPayload: payload,
};
} catch (err) {
const retryAction = getRetryAction(err);
if (retryAction === RetryAction.None) {
logger.error(
{ jobId: job.id, jobName: job.name, err },
'Fatal transaction error occurred'
);
// Not retriable so return a job result with the error details
return {
transactionError: `${err}`,
transactionPayload: undefined,
};
} }
logger.warn( const args = job.data.transactionArgs;
{ jobId: job.id, jobName: job.name, err }, let transaction: Transaction;
'Retryable transaction error occurred'
);
if (retryAction === RetryAction.WithNewTransactionId) { if (job.data.transactionState) {
logger.debug( const savedState = job.data.transactionState;
{ jobId: job.id, jobName: job.name }, logger.debug(
'Clearing saved transaction state' {
); jobId: job.id,
await updateJobData(job, undefined); jobName: job.name,
savedState,
},
'Reusing previously saved transaction state'
);
transaction = contract.deserializeTransaction(savedState);
} else {
logger.debug(
{
jobId: job.id,
jobName: job.name,
},
'Using new transaction'
);
transaction = contract.createTransaction(job.data.transactionName);
await updateJobData(job, transaction);
} }
// Rethrow the error to keep retrying logger.debug(
throw err; {
} jobId: job.id,
jobName: job.name,
transactionId: transaction.getTransactionId(),
},
'Submitting transaction'
);
try {
const payload = await submitTransaction(transaction, ...args);
return {
transactionError: undefined,
transactionPayload: payload,
};
} catch (err) {
const retryAction = getRetryAction(err);
if (retryAction === RetryAction.None) {
logger.error(
{ jobId: job.id, jobName: job.name, err },
'Fatal transaction error occurred'
);
// Not retriable so return a job result with the error details
return {
transactionError: `${err}`,
transactionPayload: undefined,
};
}
logger.warn(
{ jobId: job.id, jobName: job.name, err },
'Retryable transaction error occurred'
);
if (retryAction === RetryAction.WithNewTransactionId) {
logger.debug(
{ jobId: job.id, jobName: job.name },
'Clearing saved transaction state'
);
await updateJobData(job, undefined);
}
// Rethrow the error to keep retrying
throw err;
}
}; };
/** /**
@ -215,63 +215,63 @@ export const processSubmitTransactionJob = async (
* This manages stalled and delayed jobs and is required for retries with backoff * This manages stalled and delayed jobs and is required for retries with backoff
*/ */
export const initJobQueueScheduler = (): QueueScheduler => { export const initJobQueueScheduler = (): QueueScheduler => {
const queueScheduler = new QueueScheduler(config.JOB_QUEUE_NAME, { const queueScheduler = new QueueScheduler(config.JOB_QUEUE_NAME, {
connection, connection,
}); });
queueScheduler.on('failed', (jobId, failedReason) => { queueScheduler.on('failed', (jobId, failedReason) => {
logger.error({ jobId, failedReason }, 'Queue sceduler failure'); logger.error({ jobId, failedReason }, 'Queue sceduler failure');
}); });
return queueScheduler; return queueScheduler;
}; };
/** /**
* Helper to add a new submit transaction job to the queue * Helper to add a new submit transaction job to the queue
*/ */
export const addSubmitTransactionJob = async ( export const addSubmitTransactionJob = async (
submitQueue: Queue<JobData, JobResult>, submitQueue: Queue<JobData, JobResult>,
mspid: string, mspid: string,
transactionName: string, transactionName: string,
...transactionArgs: string[] ...transactionArgs: string[]
): Promise<string> => { ): Promise<string> => {
const jobName = `submit ${transactionName} transaction`; const jobName = `submit ${transactionName} transaction`;
const job = await submitQueue.add(jobName, { const job = await submitQueue.add(jobName, {
mspid, mspid,
transactionName, transactionName,
transactionArgs: transactionArgs, transactionArgs: transactionArgs,
transactionIds: [], transactionIds: [],
}); });
if (job?.id === undefined) { if (job?.id === undefined) {
throw new Error('Submit transaction job ID not available'); throw new Error('Submit transaction job ID not available');
} }
return job.id; return job.id;
}; };
/** /**
* Helper to update the data for an existing job * Helper to update the data for an existing job
*/ */
export const updateJobData = async ( export const updateJobData = async (
job: Job<JobData, JobResult>, job: Job<JobData, JobResult>,
transaction: Transaction | undefined transaction: Transaction | undefined
): Promise<void> => { ): Promise<void> => {
const newData = { ...job.data }; const newData = { ...job.data };
if (transaction != undefined) { if (transaction != undefined) {
const transationIds = ([] as string[]).concat( const transationIds = ([] as string[]).concat(
newData.transactionIds, newData.transactionIds,
transaction.getTransactionId() transaction.getTransactionId()
); );
newData.transactionIds = transationIds; newData.transactionIds = transationIds;
newData.transactionState = transaction.serialize(); newData.transactionState = transaction.serialize();
} else { } else {
newData.transactionState = undefined; newData.transactionState = undefined;
} }
await job.update(newData); await job.update(newData);
}; };
/** /**
@ -280,49 +280,49 @@ export const updateJobData = async (
* This function is used for the jobs REST endpoint * This function is used for the jobs REST endpoint
*/ */
export const getJobSummary = async ( export const getJobSummary = async (
queue: Queue, queue: Queue,
jobId: string jobId: string
): Promise<JobSummary> => { ): Promise<JobSummary> => {
const job: Job<JobData, JobResult> | undefined = await queue.getJob(jobId); const job: Job<JobData, JobResult> | undefined = await queue.getJob(jobId);
logger.debug({ job }, 'Got job'); logger.debug({ job }, 'Got job');
if (!(job && job.id != undefined)) { if (!(job && job.id != undefined)) {
throw new JobNotFoundError(`Job ${jobId} not found`, jobId); throw new JobNotFoundError(`Job ${jobId} not found`, jobId);
}
let transactionIds: string[];
if (job.data && job.data.transactionIds) {
transactionIds = job.data.transactionIds;
} else {
transactionIds = [];
}
let transactionError;
let transactionPayload;
const returnValue = job.returnvalue;
if (returnValue) {
if (returnValue.transactionError) {
transactionError = returnValue.transactionError;
} }
if ( let transactionIds: string[];
returnValue.transactionPayload && if (job.data && job.data.transactionIds) {
returnValue.transactionPayload.length > 0 transactionIds = job.data.transactionIds;
) {
transactionPayload = returnValue.transactionPayload.toString();
} else { } else {
transactionPayload = ''; transactionIds = [];
} }
}
const jobSummary: JobSummary = { let transactionError;
jobId: job.id, let transactionPayload;
transactionIds, const returnValue = job.returnvalue;
transactionError, if (returnValue) {
transactionPayload, if (returnValue.transactionError) {
}; transactionError = returnValue.transactionError;
}
return jobSummary; if (
returnValue.transactionPayload &&
returnValue.transactionPayload.length > 0
) {
transactionPayload = returnValue.transactionPayload.toString();
} else {
transactionPayload = '';
}
}
const jobSummary: JobSummary = {
jobId: job.id,
transactionIds,
transactionError,
transactionPayload,
};
return jobSummary;
}; };
/** /**
@ -331,16 +331,16 @@ export const getJobSummary = async (
* This function is used for the liveness REST endpoint * This function is used for the liveness REST endpoint
*/ */
export const getJobCounts = async ( export const getJobCounts = async (
queue: Queue queue: Queue
): Promise<{ [index: string]: number }> => { ): Promise<{ [index: string]: number }> => {
const jobCounts = await queue.getJobCounts( const jobCounts = await queue.getJobCounts(
'active', 'active',
'completed', 'completed',
'delayed', 'delayed',
'failed', 'failed',
'waiting' 'waiting'
); );
logger.debug({ jobCounts }, 'Current job counts'); logger.debug({ jobCounts }, 'Current job counts');
return jobCounts; return jobCounts;
}; };

View file

@ -6,5 +6,5 @@ import pino from 'pino';
import * as config from './config'; import * as config from './config';
export const logger = pino({ export const logger = pino({
level: config.logLevel, level: config.logLevel,
}); });

View file

@ -6,29 +6,32 @@ import { isMaxmemoryPolicyNoeviction } from './redis';
const mockRedisConfig = jest.fn(); const mockRedisConfig = jest.fn();
jest.mock('ioredis', () => { jest.mock('ioredis', () => {
return jest.fn().mockImplementation(() => { return jest.fn().mockImplementation(() => {
return { return {
config: mockRedisConfig, config: mockRedisConfig,
disconnect: jest.fn(), disconnect: jest.fn(),
}; };
}); });
}); });
jest.mock('./config'); jest.mock('./config');
describe('Redis', () => { describe('Redis', () => {
beforeEach(() => { beforeEach(() => {
mockRedisConfig.mockClear(); mockRedisConfig.mockClear();
});
describe('isMaxmemoryPolicyNoeviction', () => {
it('returns true when the maxmemory-policy is noeviction', async () => {
mockRedisConfig.mockReturnValue(['maxmemory-policy', 'noeviction']);
expect(await isMaxmemoryPolicyNoeviction()).toBe(true);
}); });
it('returns false when the maxmemory-policy is not noeviction', async () => { describe('isMaxmemoryPolicyNoeviction', () => {
mockRedisConfig.mockReturnValue(['maxmemory-policy', 'allkeys-lru']); it('returns true when the maxmemory-policy is noeviction', async () => {
expect(await isMaxmemoryPolicyNoeviction()).toBe(false); mockRedisConfig.mockReturnValue(['maxmemory-policy', 'noeviction']);
expect(await isMaxmemoryPolicyNoeviction()).toBe(true);
});
it('returns false when the maxmemory-policy is not noeviction', async () => {
mockRedisConfig.mockReturnValue([
'maxmemory-policy',
'allkeys-lru',
]);
expect(await isMaxmemoryPolicyNoeviction()).toBe(false);
});
}); });
});
}); });

View file

@ -16,36 +16,36 @@ import { logger } from './logger';
* For details, see: https://docs.bullmq.io/guide/connections * For details, see: https://docs.bullmq.io/guide/connections
*/ */
export const isMaxmemoryPolicyNoeviction = async (): Promise<boolean> => { export const isMaxmemoryPolicyNoeviction = async (): Promise<boolean> => {
let redis: Redis | undefined; let redis: Redis | undefined;
const redisOptions: RedisOptions = { const redisOptions: RedisOptions = {
port: config.redisPort, port: config.redisPort,
host: config.redisHost, host: config.redisHost,
username: config.redisUsername, username: config.redisUsername,
password: config.redisPassword, password: config.redisPassword,
}; };
try { try {
redis = new IORedis(redisOptions); redis = new IORedis(redisOptions);
const maxmemoryPolicyConfig = await (redis as Redis).config( const maxmemoryPolicyConfig = await (redis as Redis).config(
'GET', 'GET',
'maxmemory-policy' 'maxmemory-policy'
); );
logger.debug({ maxmemoryPolicyConfig }, 'Got maxmemory-policy config'); logger.debug({ maxmemoryPolicyConfig }, 'Got maxmemory-policy config');
if ( if (
maxmemoryPolicyConfig.length == 2 && maxmemoryPolicyConfig.length == 2 &&
'maxmemory-policy' === maxmemoryPolicyConfig[0] && 'maxmemory-policy' === maxmemoryPolicyConfig[0] &&
'noeviction' === maxmemoryPolicyConfig[1] 'noeviction' === maxmemoryPolicyConfig[1]
) { ) {
return true; return true;
}
} finally {
if (redis != undefined) {
redis.disconnect();
}
} }
} finally {
if (redis != undefined) {
redis.disconnect();
}
}
return false; return false;
}; };

View file

@ -18,70 +18,70 @@ import cors from 'cors';
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND } = StatusCodes; const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND } = StatusCodes;
export const createServer = async (): Promise<Application> => { export const createServer = async (): Promise<Application> => {
const app = express(); const app = express();
app.use( app.use(
pinoMiddleware({ pinoMiddleware({
logger, logger,
customLogLevel: function customLogLevel(res, err) { customLogLevel: function customLogLevel(res, err) {
if ( if (
res.statusCode >= BAD_REQUEST && res.statusCode >= BAD_REQUEST &&
res.statusCode < INTERNAL_SERVER_ERROR res.statusCode < INTERNAL_SERVER_ERROR
) { ) {
return 'warn'; return 'warn';
} }
if (res.statusCode >= INTERNAL_SERVER_ERROR || err) { if (res.statusCode >= INTERNAL_SERVER_ERROR || err) {
return 'error'; return 'error';
} }
return 'debug'; return 'debug';
}, },
}) })
); );
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// define passport startegy // define passport startegy
passport.use(fabricAPIKeyStrategy); passport.use(fabricAPIKeyStrategy);
// initialize passport js // initialize passport js
app.use(passport.initialize()); app.use(passport.initialize());
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
app.use(cors()); app.use(cors());
} }
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
// TBC // TBC
} }
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
app.use(helmet()); app.use(helmet());
} }
app.use('/', healthRouter); app.use('/', healthRouter);
app.use('/api/assets', authenticateApiKey, assetsRouter); app.use('/api/assets', authenticateApiKey, assetsRouter);
app.use('/api/jobs', authenticateApiKey, jobsRouter); app.use('/api/jobs', authenticateApiKey, jobsRouter);
app.use('/api/transactions', authenticateApiKey, transactionsRouter); app.use('/api/transactions', authenticateApiKey, transactionsRouter);
// For everything else // For everything else
app.use((_req, res) => app.use((_req, res) =>
res.status(NOT_FOUND).json({ res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND), status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
); );
// Print API errors // Print API errors
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
logger.error(err); logger.error(err);
return res.status(INTERNAL_SERVER_ERROR).json({ return res.status(INTERNAL_SERVER_ERROR).json({
status: getReasonPhrase(INTERNAL_SERVER_ERROR), status: getReasonPhrase(INTERNAL_SERVER_ERROR),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
});
}); });
});
return app; return app;
}; };

View file

@ -14,42 +14,46 @@ const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
export const transactionsRouter = express.Router(); export const transactionsRouter = express.Router();
transactionsRouter.get( transactionsRouter.get(
'/:transactionId', '/:transactionId',
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const mspId = req.user as string; const mspId = req.user as string;
const transactionId = req.params.transactionId; const transactionId = req.params.transactionId;
logger.debug('Read request received for transaction ID %s', transactionId); logger.debug(
'Read request received for transaction ID %s',
try { transactionId
const qsccContract = req.app.locals[mspId]?.qsccContract as Contract;
const validationCode = await getTransactionValidationCode(
qsccContract,
transactionId
);
return res.status(OK).json({
transactionId,
validationCode,
});
} catch (err) {
if (err instanceof TransactionNotFoundError) {
return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(),
});
} else {
logger.error(
{ err },
'Error processing read request for transaction ID %s',
transactionId
); );
return res.status(INTERNAL_SERVER_ERROR).json({ try {
status: getReasonPhrase(INTERNAL_SERVER_ERROR), const qsccContract = req.app.locals[mspId]
timestamp: new Date().toISOString(), ?.qsccContract as Contract;
});
} const validationCode = await getTransactionValidationCode(
qsccContract,
transactionId
);
return res.status(OK).json({
transactionId,
validationCode,
});
} catch (err) {
if (err instanceof TransactionNotFoundError) {
return res.status(NOT_FOUND).json({
status: getReasonPhrase(NOT_FOUND),
timestamp: new Date().toISOString(),
});
} else {
logger.error(
{ err },
'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(),
});
}
}
} }
}
); );

View file

@ -1,46 +0,0 @@
{
"env": {
"node": true,
"es2020": true
},
"root": true,
"ignorePatterns": [
"dist/"
],
"extends": [
"eslint:recommended"
],
"rules": {
"indent": [
"error",
4
],
"quotes": [
"error",
"single"
]
},
"overrides": [
{
"files": [
"**/*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
},
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
]
}
]
}

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -14,7 +14,7 @@ const chaincodeName = 'events';
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
const now = Date.now(); const now = Date.now();
const assetId = `asset${now}`; const assetId = `asset${String(now)}`;
async function main(): Promise<void> { async function main(): Promise<void> {
@ -60,7 +60,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -102,7 +102,7 @@ async function createAsset(contract: Contract): Promise<bigint> {
const status = await result.getStatus(); const status = await result.getStatus();
if (!status.successful) { if (!status.successful) {
throw new Error(`failed to commit transaction ${status.transactionId} with status code ${status.code}`); throw new Error(`failed to commit transaction ${status.transactionId} with status code ${String(status.code)}`);
} }
console.log('\n*** CreateAsset committed successfully'); console.log('\n*** CreateAsset committed successfully');

View file

@ -50,5 +50,9 @@ export async function newSigner(): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
return path.join(dirPath, files[0]); const file = files[0];
if (!file) {
throw new Error(`No files in directory: ${dirPath}`);
}
return path.join(dirPath, file);
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "outDir": "dist",
"emitDecoratorMetadata": true, "declaration": true,
"outDir": "dist", "declarationMap": true,
"declaration": true, "sourceMap": true,
"sourceMap": true, "noUnusedLocals": true,
"noImplicitAny": true "noImplicitReturns": true,
}, "noUncheckedIndexedAccess": true,
"include": [ "forceConsistentCasingInFileNames": true
"./src/**/*" },
], "include": ["./src/**/*"],
"exclude": [ "exclude": ["./src/**/*.spec.ts"]
"./src/**/*.spec.ts"
]
} }

View file

@ -1,45 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"root": true,
"ignorePatterns": [
"dist/"
],
"extends": [
"eslint:recommended"
],
"rules": {
"indent": [
"error",
4
],
"quotes": [
"error",
"single"
]
},
"overrides": [
{
"files": [
"**/*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
]
}

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -27,8 +27,8 @@ const RESET = '\x1b[0m';
// Use a unique key so that we can run multiple times // Use a unique key so that we can run multiple times
const now = Date.now(); const now = Date.now();
const assetID1 = `asset${now}`; const assetID1 = `asset${String(now)}`;
const assetID2 = `asset${now + 1}`; const assetID2 = `asset${String(now + 1)}`;
async function main(): Promise<void> { async function main(): Promise<void> {
const clientOrg1 = await newGrpcConnection( const clientOrg1 = await newGrpcConnection(
@ -74,14 +74,13 @@ async function main(): Promise<void> {
// Read asset from the Org1's private data collection with ID in the given range. // Read asset from the Org1's private data collection with ID in the given range.
await getAssetsByRange(contractOrg1); await getAssetsByRange(contractOrg1);
try{ try {
// Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail. // Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail.
console.log('\nAttempt TransferAsset without prior AgreeToTransfer'); console.log('\nAttempt TransferAsset without prior AgreeToTransfer');
await transferAsset(contractOrg1, assetID1); await transferAsset(contractOrg1, assetID1);
doFail('TransferAsset transaction succeeded when it was expected to fail'); doFail('TransferAsset transaction succeeded when it was expected to fail');
} } catch (e) {
catch(e){ console.log('*** Received expected error:', e);
console.log(`*** Received expected error: ${e}`);
} }
console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~'); console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~');
@ -122,7 +121,7 @@ async function main(): Promise<void> {
await deleteAsset(contractOrg2, assetID2); await deleteAsset(contractOrg2, assetID2);
doFail('DeleteAsset transaction succeeded when it was expected to fail'); doFail('DeleteAsset transaction succeeded when it was expected to fail');
} catch (e) { } catch (e) {
console.log(`*** Received expected error: ${e}`); console.log('*** Received expected error:', e);
} }
console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~');
@ -142,7 +141,7 @@ async function main(): Promise<void> {
} }
} }
main().catch((error) => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });
@ -192,14 +191,14 @@ async function getAssetsByRange(contract: Contract): Promise<void> {
const resultBytes = await contract.evaluateTransaction( const resultBytes = await contract.evaluateTransaction(
'GetAssetByRange', 'GetAssetByRange',
assetID1, assetID1,
`asset${now + 2}` `asset${String(now + 2)}`
); );
const resultString = utf8Decoder.decode(resultBytes); const resultString = utf8Decoder.decode(resultBytes);
if (!resultString) { if (!resultString) {
doFail('Received empty query list for readAssetPrivateDetailsOrg1'); doFail('Received empty query list for readAssetPrivateDetailsOrg1');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -211,7 +210,7 @@ async function readAssetByID(contract: Contract, assetID: string): Promise<void>
if (!resultString) { if (!resultString) {
doFail('Received empty result for ReadAsset'); doFail('Received empty result for ReadAsset');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -241,7 +240,7 @@ async function readTransferAgreement(contract: Contract, assetID: string): Promi
if (!resultString) { if (!resultString) {
doFail('Received no result for ReadTransferAgreement'); doFail('Received no result for ReadTransferAgreement');
} }
const result = JSON.parse(resultString); const result: unknown = JSON.parse(resultString);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -290,7 +289,7 @@ async function readAssetPrivateDetails(contract: Contract, assetID: string, coll
console.log('*** No result'); console.log('*** No result');
return false; return false;
} }
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
return true; return true;
} }

View file

@ -127,5 +127,9 @@ export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
return path.join(dirPath, files[0]); const file = files[0];
if (!file) {
throw new Error(`No files in directory: ${dirPath}`);
}
return path.join(dirPath, file);
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "outDir": "dist",
"emitDecoratorMetadata": true, "declaration": true,
"outDir": "dist", "declarationMap": true,
"declaration": true, "sourceMap": true,
"sourceMap": true, "noUnusedLocals": true,
"noImplicitAny": true "noImplicitReturns": true,
}, "noUncheckedIndexedAccess": true,
"include": [ "forceConsistentCasingInFileNames": true
"./src/**/*" },
], "include": ["./src/**/*"],
"exclude": [ "exclude": ["./src/**/*.spec.ts"]
"./src/**/*.spec.ts"
]
} }

View file

@ -91,9 +91,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.4.0", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz",
"integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==", "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View file

@ -1,102 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"root": true,
"ignorePatterns": [
"dist/"
],
"extends": [
"eslint:recommended"
],
"rules": {
"indent": [
"error",
4
],
"quotes": [
"error",
"single"
]
},
"overrides": [
{
"files": [
"**/*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
},
"project": [
"./tsconfig.json"
]
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/comma-spacing": [
"error"
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"@typescript-eslint/func-call-spacing": [
"error"
],
"@typescript-eslint/member-delimiter-style": [
"error"
],
"@typescript-eslint/indent": [
"error",
4,
{
"SwitchCase": 0
}
],
"@typescript-eslint/prefer-nullish-coalescing": [
"error"
],
"@typescript-eslint/prefer-optional-chain": [
"error"
],
"@typescript-eslint/prefer-reduce-type-parameter": [
"error"
],
"@typescript-eslint/prefer-return-this-type": [
"error"
],
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/type-annotation-spacing": [
"error"
],
"@typescript-eslint/semi": [
"error"
],
"@typescript-eslint/space-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
]
}
}
]
}

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node dist/app.js" "start": "node dist/app.js"
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6", "@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0", "eslint": "^8.57.0",
"@typescript-eslint/parser": "^6.9.0", "typescript": "~5.4",
"eslint": "^8.52.0", "typescript-eslint": "^7.13.0"
"typescript": "~5.2.2"
} }
} }

View file

@ -72,8 +72,8 @@ async function main(): Promise<void> {
// Org2 is not the owner and does not have the private details, read expected to fail. // Org2 is not the owner and does not have the private details, read expected to fail.
try { try {
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1); await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1);
} catch(e) { } catch (e) {
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`);
} }
// Org1 updates the assets public description. // Org1 updates the assets public description.
@ -94,7 +94,7 @@ async function main(): Promise<void> {
ownerOrg: mspIdOrg1, ownerOrg: mspIdOrg1,
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`}); publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${String(e)}${RESET}`);
} }
// Read the public details by org1. // Read the public details by org1.
@ -126,14 +126,14 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1); await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1);
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${String(e)}${RESET}`);
} }
// Org1 has not agreed to buy so this should fail. // Org1 has not agreed to buy so this should fail.
try{ try{
await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2); await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${String(e)}${RESET}`);
} }
// Org2 should be able to see the price it has agreed. // Org2 should be able to see the price it has agreed.
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2); await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
@ -143,7 +143,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`);
} }
// Agree to a sell by Org1, the seller will agree to the bid price of Org2. // Agree to a sell by Org1, the seller will agree to the bid price of Org2.
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}); await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
@ -168,7 +168,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`);
} }
// Org1 will transfer the asset to Org2. // Org1 will transfer the asset to Org2.
@ -188,7 +188,7 @@ async function main(): Promise<void> {
try{ try{
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2); await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`); console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`);
} }
// This is an update to the public state and requires only the owner to endorse. // This is an update to the public state and requires only the owner to endorse.
@ -209,7 +209,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });

View file

@ -103,5 +103,9 @@ export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
async function getFirstDirFileName(dirPath: string): Promise<string> { async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
return path.join(dirPath, files[0]); const file = files[0];
if (!file) {
throw new Error(`No files in directory: ${dirPath}`);
}
return path.join(dirPath, file);
} }

View file

@ -6,10 +6,10 @@
import { Contract } from '@hyperledger/fabric-gateway'; import { Contract } from '@hyperledger/fabric-gateway';
import { TextDecoder } from 'util'; import { TextDecoder } from 'util';
import { GREEN, parse, RED, RESET } from './utils'; import { GREEN, parse, RED, RESET } from './utils';
import crpto from 'crypto'; import crypto from 'crypto';
import { mspIdOrg2 } from './connect'; import { mspIdOrg2 } from './connect';
const randomBytes = crpto.randomBytes(256).toString('hex'); const randomBytes = crypto.randomBytes(256).toString('hex');
interface AssetJSON { interface AssetJSON {
objectType: string; objectType: string;
@ -151,7 +151,7 @@ export class ContractWrapper {
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId] endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
}); });
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`); console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${String(assetPrice.price)}`);
} }
public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> { public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
@ -169,16 +169,11 @@ export class ContractWrapper {
const resultString = this.#utf8Decoder.decode(resultBytes); const resultString = this.#utf8Decoder.decode(resultBytes);
if (resultString.length !== 0) { if (resultString.length !== 0) {
const json = parse<AssetPropertiesJSON>(resultString); const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = { if (typeof json === 'object') {
color: json.color,
size: json.size
};
if (result) {
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`); console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
} else { } else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`); console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
} }
} else { } else {
throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`); throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
} }

View file

@ -9,5 +9,5 @@ export const GREEN = '\x1b[32m\n';
export const RESET = '\x1b[0m'; export const RESET = '\x1b[0m';
export function parse<T>(data: string): T { export function parse<T>(data: string): T {
return JSON.parse(data); return JSON.parse(data) as T;
} }

View file

@ -1,17 +1,15 @@
{ {
"extends":"@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "outDir": "dist",
"emitDecoratorMetadata": true, "declaration": true,
"outDir": "dist", "declarationMap": true,
"declaration": true, "sourceMap": true,
"sourceMap": true, "noUnusedLocals": true,
"noImplicitAny": true "noImplicitReturns": true,
}, "noUncheckedIndexedAccess": true,
"include": [ "forceConsistentCasingInFileNames": true
"./src/**/*" },
], "include": ["./src/**/*"],
"exclude": [ "exclude": ["./src/**/*.spec.ts"]
"./src/**/*.spec.ts"
]
} }

View file

@ -19,8 +19,8 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
}, },

View file

@ -18,8 +18,8 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"env-var": "^7.1.1", "env-var": "^7.1.1",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"

View file

@ -29,8 +29,8 @@
"typescript": "~5.2.2" "typescript": "~5.2.2"
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.16.3" "express": "^4.16.3"

View file

@ -1,99 +0,0 @@
module.exports = {
env: {
node: true,
es2021: true,
},
extends: [
'eslint:recommended',
],
root: true,
ignorePatterns: [
'dist/',
],
rules: {
'arrow-spacing': ['error'],
'comma-style': ['error'],
complexity: ['error', 10],
'eol-last': ['error'],
'generator-star-spacing': ['error', 'after'],
'key-spacing': [
'error',
{
beforeColon: false,
afterColon: true,
mode: 'minimum',
},
],
'keyword-spacing': ['error'],
'no-multiple-empty-lines': ['error'],
'no-trailing-spaces': ['error'],
'no-whitespace-before-property': ['error'],
'object-curly-newline': ['error'],
'padded-blocks': ['error', 'never'],
'rest-spread-spacing': ['error'],
'semi-style': ['error'],
'space-before-blocks': ['error'],
'space-in-parens': ['error'],
'space-unary-ops': ['error'],
'spaced-comment': ['error'],
'template-curly-spacing': ['error'],
'yield-star-spacing': ['error', 'after'],
},
overrides: [
{
files: [
'**/*.ts',
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
},
project: './tsconfig.json',
tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname,
},
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'@typescript-eslint/comma-spacing': ['error'],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
},
],
'@typescript-eslint/func-call-spacing': ['error'],
'@typescript-eslint/member-delimiter-style': ['error'],
'@typescript-eslint/indent': [
'error',
4,
{
SwitchCase: 0,
},
],
'@typescript-eslint/prefer-nullish-coalescing': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'@typescript-eslint/prefer-reduce-type-parameter': ['error'],
'@typescript-eslint/prefer-return-this-type': ['error'],
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/type-annotation-spacing': ['error'],
'@typescript-eslint/semi': ['error'],
'@typescript-eslint/space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
},
},
],
};

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint ./src", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node ./dist/app", "start": "node ./dist/app",
@ -19,15 +19,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0", "typescript": "~5.4.5",
"typescript": "~5.2.2" "typescript-eslint": "^7.11.0"
} }
} }

View file

@ -12,10 +12,10 @@ async function main(): Promise<void> {
const commandName = process.argv[2]; const commandName = process.argv[2];
const args = process.argv.slice(3); const args = process.argv.slice(3);
const command = commands[commandName]; const command = commandName && commands[commandName];
if (!command) { if (!command) {
printUsage(); printUsage();
throw new Error(`Unknown command: ${commandName}`); throw new Error(`Unknown command: ${String(commandName)}`);
} }
await runCommand(command, args); await runCommand(command, args);
@ -41,7 +41,7 @@ function printUsage(): void {
console.log(`\t${Object.keys(commands).sort().join('\n\t')}`); console.log(`\t${Object.keys(commands).sort().join('\n\t')}`);
} }
main().catch(error => { main().catch((error: unknown) => {
if (error instanceof ExpectedError) { if (error instanceof ExpectedError) {
console.log(error); console.log(error);
} else { } else {

View file

@ -7,10 +7,14 @@
import { Gateway } from '@hyperledger/fabric-gateway'; import { Gateway } from '@hyperledger/fabric-gateway';
import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config';
import { AssetTransfer } from '../contract'; import { AssetTransfer } from '../contract';
import { assertAllDefined } from '../utils'; import { assertDefined } from '../utils';
const usage = 'Arguments: <assetId> <ownerName> <color>';
export default async function main(gateway: Gateway, args: string[]): Promise<void> { export default async function main(gateway: Gateway, args: string[]): Promise<void> {
const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <color>'); const assetId = assertDefined(args[0], usage);
const owner = assertDefined(args[1], usage);
const color = assertDefined(args[2], usage);
const network = gateway.getNetwork(CHANNEL_NAME); const network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME); const contract = network.getContract(CHAINCODE_NAME);

View file

@ -16,5 +16,8 @@ export default async function main(gateway: Gateway): Promise<void> {
const assets = await smartContract.getAllAssets(); const assets = await smartContract.getAllAssets();
const assetsJson = JSON.stringify(assets, undefined, 2); const assetsJson = JSON.stringify(assets, undefined, 2);
assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation // Write line-by-line to avoid truncation
assetsJson.split('\n').forEach((line) => {
console.log(line);
});
} }

View file

@ -19,10 +19,10 @@ export default async function main(gateway: Gateway): Promise<void> {
const network = gateway.getNetwork(CHANNEL_NAME); const network = gateway.getNetwork(CHANNEL_NAME);
const checkpointer = await checkpointers.file(checkpointFile); const checkpointer = await checkpointers.file(checkpointFile);
console.log(`Starting event listening from block ${checkpointer.getBlockNumber() ?? startBlock}`); console.log('Starting event listening from block', checkpointer.getBlockNumber() ?? startBlock);
console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId());
if (simulatedFailureCount > 0) { if (simulatedFailureCount > 0) {
console.log(`Simulating a write failure every ${simulatedFailureCount} transactions`); console.log('Simulating a write failure every', simulatedFailureCount, 'transactions');
} }
const events = await network.getChaincodeEvents(CHAINCODE_NAME, { const events = await network.getChaincodeEvents(CHAINCODE_NAME, {

View file

@ -7,10 +7,14 @@
import { Gateway } from '@hyperledger/fabric-gateway'; import { Gateway } from '@hyperledger/fabric-gateway';
import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; import { CHAINCODE_NAME, CHANNEL_NAME } from '../config';
import { AssetTransfer } from '../contract'; import { AssetTransfer } from '../contract';
import { assertAllDefined } from '../utils'; import { assertDefined } from '../utils';
const usage = 'Arguments: <assetId> <ownerName> <ownerMspId>';
export default async function main(gateway: Gateway, args: string[]): Promise<void> { export default async function main(gateway: Gateway, args: string[]): Promise<void> {
const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <ownerMspId>'); const assetId = assertDefined(args[0], usage);
const newOwner = assertDefined(args[1], usage);
const newOwnerOrg = assertDefined(args[2], usage);
const network = gateway.getNetwork(CHANNEL_NAME); const network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME); const contract = network.getContract(CHAINCODE_NAME);

View file

@ -13,7 +13,8 @@ const utf8Decoder = new TextDecoder();
* @param values Candidate elements. * @param values Candidate elements.
*/ */
export function randomElement<T>(values: T[]): T { export function randomElement<T>(values: T[]): T {
return values[randomInt(values.length)]; const result = values[randomInt(values.length)];
return assertDefined(result, `Missing element in {String(values)}`);
} }
/** /**
@ -47,7 +48,7 @@ export async function allFulfilled(promises: Promise<unknown>[]): Promise<void>
if (failures.length > 0) { if (failures.length > 0) {
const failMessages = '- ' + failures.join('\n- '); const failMessages = '- ' + failures.join('\n- ');
throw new Error(`${failures.length} failures:\n${failMessages}\n`); throw new Error(`${String(failures.length)} failures:\n${failMessages}\n`);
} }
} }
@ -61,11 +62,6 @@ export function printable<T extends object>(event: T): PrintView<T> {
) as PrintView<T>; ) as PrintView<T>;
} }
export function assertAllDefined<T>(values: (T | undefined)[], message: string | (() => string)): T[] {
values.forEach(value => assertDefined(value, message));
return values as T[];
}
export function assertDefined<T>(value: T | undefined, message: string | (() => string)): T { export function assertDefined<T>(value: T | undefined, message: string | (() => string)): T {
if (value == undefined) { if (value == undefined) {
throw new Error(typeof message === 'string' ? message : message()); throw new Error(typeof message === 'string' ? message : message());

View file

@ -1,19 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }

View file

@ -1,101 +0,0 @@
module.exports = {
env: {
node: true,
es2021: true,
},
extends: [
'eslint:recommended',
],
root: true,
ignorePatterns: [
'dist/',
],
rules: {
'arrow-spacing': ['error'],
'comma-style': ['error'],
complexity: ['error', 10],
'eol-last': ['error'],
'generator-star-spacing': ['error', 'after'],
'key-spacing': [
'error',
{
beforeColon: false,
afterColon: true,
mode: 'minimum',
},
],
'keyword-spacing': ['error'],
'no-multiple-empty-lines': ['error'],
'no-trailing-spaces': ['error'],
'no-whitespace-before-property': ['error'],
'object-curly-newline': ['error'],
'padded-blocks': ['error', 'never'],
'rest-spread-spacing': ['error'],
'semi-style': ['error'],
'space-before-blocks': ['error'],
'space-in-parens': ['error'],
'space-unary-ops': ['error'],
'spaced-comment': ['error'],
'template-curly-spacing': ['error'],
'yield-star-spacing': ['error', 'after'],
},
overrides: [
{
files: [
'**/*.ts',
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
},
project: './tsconfig.json',
tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname,
},
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'@typescript-eslint/comma-spacing': ['error'],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
},
],
'@typescript-eslint/func-call-spacing': ['error'],
'@typescript-eslint/member-delimiter-style': ['error'],
'@typescript-eslint/indent': [
'error',
4,
{
SwitchCase: 0,
ignoredNodes: ["PropertyDefinition"]
},
],
'@typescript-eslint/prefer-nullish-coalescing': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'@typescript-eslint/prefer-reduce-type-parameter': ['error'],
'@typescript-eslint/prefer-return-this-type': ['error'],
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/type-annotation-spacing': ['error'],
'@typescript-eslint/semi': ['error'],
'@typescript-eslint/space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
},
},
],
};

View file

@ -10,7 +10,6 @@ coverage
node_modules/ node_modules/
jspm_packages/ jspm_packages/
package-lock.json package-lock.json
npm-shrinkwrap.json
# Compiled TypeScript files # Compiled TypeScript files
dist dist

View file

@ -1,7 +1,7 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
FROM node:18.16 AS builder FROM node:18 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,10 @@
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"engines": { "engines": {
"node": ">=18.12.0" "node": ">=18"
}, },
"scripts": { "scripts": {
"lint": "eslint ./src --ext .ts", "lint": "eslint src",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "", "test": "",
"start": "set -x && fabric-chaincode-node start", "start": "set -x && fabric-chaincode-node start",
@ -32,12 +32,12 @@
"sort-keys-recursive": "^2.1.7" "sort-keys-recursive": "^2.1.7"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^2.0.0", "@types/node": "^18.19.33",
"@types/node": "^18.16.1", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^5.30.7", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^5.30.7", "eslint": "^8.57.0",
"eslint": "^8.20.0", "typescript": "~5.4.5",
"typescript": "~5.0.4" "typescript-eslint": "^7.11.0"
}, },
"nyc": { "nyc": {
"extension": [ "extension": [

View file

@ -21,10 +21,6 @@ export class Asset {
@Property('Size', 'number') @Property('Size', 'number')
Size = 0; Size = 0;
constructor() {
// Nothing to do
}
static newInstance(state: Partial<Asset> = {}): Asset { static newInstance(state: Partial<Asset> = {}): Asset {
return { return {
ID: assertHasValue(state.ID, 'Missing ID'), ID: assertHasValue(state.ID, 'Missing ID'),

View file

@ -50,7 +50,7 @@ export class AssetTransferContract extends Contract {
async #readAsset(ctx: Context, id: string): Promise<Uint8Array> { async #readAsset(ctx: Context, id: string): Promise<Uint8Array> {
const assetBytes = await ctx.stub.getState(id); // get the asset from chaincode state const assetBytes = await ctx.stub.getState(id); // get the asset from chaincode state
if (!assetBytes || assetBytes.length === 0) { if (assetBytes.length === 0) {
throw new Error(`Sorry, asset ${id} has not been created`); throw new Error(`Sorry, asset ${id} has not been created`);
} }
@ -64,7 +64,7 @@ export class AssetTransferContract extends Contract {
@Transaction() @Transaction()
@Param('assetObj', 'Asset', 'Part formed JSON of Asset') @Param('assetObj', 'Asset', 'Part formed JSON of Asset')
async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise<void> { async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise<void> {
if (assetUpdate.ID === undefined) { if (!assetUpdate.ID) {
throw new Error('No asset ID specified'); throw new Error('No asset ID specified');
} }
@ -113,7 +113,7 @@ export class AssetTransferContract extends Contract {
@Returns('boolean') @Returns('boolean')
async AssetExists(ctx: Context, id: string): Promise<boolean> { async AssetExists(ctx: Context, id: string): Promise<boolean> {
const assetJson = await ctx.stub.getState(id); const assetJson = await ctx.stub.getState(id);
return assetJson?.length > 0; return assetJson.length > 0;
} }
/** /**

View file

@ -4,16 +4,14 @@
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/"]
"./src/**/*"
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -1,29 +0,0 @@
env:
node: true
es2020: true
root: true
ignorePatterns:
- dist/
extends:
- eslint:recommended
rules:
indent:
- error
- 4
quotes:
- error
- single
overrides:
- files:
- "**/*.ts"
parser: "@typescript-eslint/parser"
parserOptions:
sourceType: module
ecmaFeatures:
impliedStrict: true
plugins:
- "@typescript-eslint"
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,24 +10,24 @@
"build": "tsc", "build": "tsc",
"prepare": "npm run build", "prepare": "npm run build",
"clean": "rimraf dist", "clean": "rimraf dist",
"lint": "eslint . --ext .ts", "lint": "eslint src",
"start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js", "start": "SOFTHSM2_CONF=${HOME}/softhsm2.conf node dist/hsm-sample.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0" "@hyperledger/fabric-gateway": "^1.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"typescript": "~5.2.2" "typescript": "~5.4.5",
"typescript-eslint": "^7.11.0"
} }
} }

View file

@ -13,7 +13,7 @@ import { TextDecoder } from 'util';
const mspId = 'Org1MSP'; const mspId = 'Org1MSP';
const user = 'HSMUser'; const user = 'HSMUser';
const assetId = `asset${Date.now()}`; const assetId = `asset${String(Date.now())}`;
const utf8Decoder = new TextDecoder(); const utf8Decoder = new TextDecoder();
// Sample uses fabric-ca-client generated HSM identities, certificate is located in the signcerts directory // Sample uses fabric-ca-client generated HSM identities, certificate is located in the signcerts directory
@ -85,7 +85,7 @@ async function exampleTransaction(gateway: Gateway):Promise<void> {
const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);
const resultJson = utf8Decoder.decode(resultBytes); const resultJson = utf8Decoder.decode(resultBytes);
const result = JSON.parse(resultJson); const result: unknown = JSON.parse(resultJson);
console.log('*** Result:', result); console.log('*** Result:', result);
} }
@ -167,7 +167,7 @@ function envOrDefault(key: string, defaultValue: string): string {
return process.env[key] || defaultValue; return process.env[key] || defaultValue;
} }
main().catch(error => { main().catch((error: unknown) => {
console.error('******** FAILED to run the application:', error); console.error('******** FAILED to run the application:', error);
process.exitCode = 1; process.exitCode = 1;
}); });

View file

@ -1,18 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
]
} }

View file

@ -1,99 +0,0 @@
module.exports = {
env: {
node: true,
es2020: true,
},
extends: [
'eslint:recommended',
],
root: true,
ignorePatterns: [
'dist/',
],
rules: {
'arrow-spacing': ['error'],
'comma-style': ['error'],
complexity: ['error', 10],
'eol-last': ['error'],
'generator-star-spacing': ['error', 'after'],
'key-spacing': [
'error',
{
beforeColon: false,
afterColon: true,
mode: 'minimum',
},
],
'keyword-spacing': ['error'],
'no-multiple-empty-lines': ['error'],
'no-trailing-spaces': ['error'],
'no-whitespace-before-property': ['error'],
'object-curly-newline': ['error'],
'padded-blocks': ['error', 'never'],
'rest-spread-spacing': ['error'],
'semi-style': ['error'],
'space-before-blocks': ['error'],
'space-in-parens': ['error'],
'space-unary-ops': ['error'],
'spaced-comment': ['error'],
'template-curly-spacing': ['error'],
'yield-star-spacing': ['error', 'after'],
},
overrides: [
{
files: [
'**/*.ts',
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
},
project: './tsconfig.json',
tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname,
},
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
rules: {
'@typescript-eslint/comma-spacing': ['error'],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
},
],
'@typescript-eslint/func-call-spacing': ['error'],
'@typescript-eslint/member-delimiter-style': ['error'],
'@typescript-eslint/indent': [
'error',
4,
{
SwitchCase: 0,
},
],
'@typescript-eslint/prefer-nullish-coalescing': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'@typescript-eslint/prefer-reduce-type-parameter': ['error'],
'@typescript-eslint/prefer-return-this-type': ['error'],
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/type-annotation-spacing': ['error'],
'@typescript-eslint/semi': ['error'],
'@typescript-eslint/space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
},
},
],
};

View file

@ -0,0 +1,13 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
});

View file

@ -10,7 +10,7 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
"lint": "eslint ./src --ext .ts", "lint": "eslint src",
"prepare": "npm run build", "prepare": "npm run build",
"pretest": "npm run lint", "pretest": "npm run lint",
"start": "node ./dist/app" "start": "node ./dist/app"
@ -18,16 +18,16 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.9.7", "@grpc/grpc-js": "^1.10",
"@hyperledger/fabric-gateway": "~1.4.0", "@hyperledger/fabric-gateway": "^1.5",
"@hyperledger/fabric-protos": "^0.2.1" "@hyperledger/fabric-protos": "^0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.18.6", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.9.0", "eslint": "^8.57.0",
"eslint": "^8.52.0", "typescript": "~5.4.5",
"typescript": "~5.2.2" "typescript-eslint": "^7.11.0"
} }
} }

View file

@ -47,7 +47,7 @@ function printUsage(): void {
console.log('Available commands:', Object.keys(allCommands).join(', ')); console.log('Available commands:', Object.keys(allCommands).join(', '));
} }
main().catch(error => { main().catch((error: unknown) => {
if (error instanceof ExpectedError) { if (error instanceof ExpectedError) {
console.log(error); console.log(error);
} else { } else {

View file

@ -35,10 +35,18 @@ export function parseBlock(block: common.Block): Block {
return { return {
getNumber: () => BigInt(header.getNumber()), getNumber: () => BigInt(header.getNumber()),
getTransactions: cache( getTransactions: cache(() =>
() => getPayloads(block) getPayloads(block)
.map((payload, i) => parsePayload(payload, validationCodes[i])) .map((payload, i) =>
.filter(payload => payload.isEndorserTransaction()) parsePayload(
payload,
assertDefined(
validationCodes[i],
`Missing validation code index {String(i)}`
)
)
)
.filter((payload) => payload.isEndorserTransaction())
.map(newTransaction) .map(newTransaction)
), ),
toProto: () => block, toProto: () => block,
@ -67,15 +75,23 @@ interface ReadWriteSet {
function parsePayload(payload: common.Payload, statusCode: number): Payload { function parsePayload(payload: common.Payload, statusCode: number): Payload {
const cachedChannelHeader = cache(() => getChannelHeader(payload)); const cachedChannelHeader = cache(() => getChannelHeader(payload));
const isEndorserTransaction = (): boolean => cachedChannelHeader().getType() === common.HeaderType.ENDORSER_TRANSACTION; const isEndorserTransaction = (): boolean =>
cachedChannelHeader().getType() ===
common.HeaderType.ENDORSER_TRANSACTION;
return { return {
getChannelHeader: cachedChannelHeader, getChannelHeader: cachedChannelHeader,
getEndorserTransaction: () => { getEndorserTransaction: () => {
if (!isEndorserTransaction()) { if (!isEndorserTransaction()) {
throw new Error(`Unexpected payload type: ${cachedChannelHeader().getType()}`); throw new Error(
`Unexpected payload type: ${String(
cachedChannelHeader().getType()
)}`
);
} }
const transaction = peer.Transaction.deserializeBinary(payload.getData_asU8()); const transaction = peer.Transaction.deserializeBinary(
payload.getData_asU8()
);
return parseEndorserTransaction(transaction); return parseEndorserTransaction(transaction);
}, },
getSignatureHeader: cache(() => getSignatureHeader(payload)), getSignatureHeader: cache(() => getSignatureHeader(payload)),
@ -86,16 +102,33 @@ function parsePayload(payload: common.Payload, statusCode: number): Payload {
}; };
} }
function parseEndorserTransaction(transaction: peer.Transaction): EndorserTransaction { function parseEndorserTransaction(
transaction: peer.Transaction
): EndorserTransaction {
return { return {
getReadWriteSets: cache( getReadWriteSets: cache(() =>
() => getChaincodeActionPayloads(transaction) getChaincodeActionPayloads(transaction)
.map(payload => assertDefined(payload.getAction(), 'Missing chaincode endorsed action')) .map((payload) =>
.map(endorsedAction => endorsedAction.getProposalResponsePayload_asU8()) assertDefined(
.map(bytes => peer.ProposalResponsePayload.deserializeBinary(bytes)) payload.getAction(),
.map(responsePayload => peer.ChaincodeAction.deserializeBinary(responsePayload.getExtension_asU8())) 'Missing chaincode endorsed action'
.map(chaincodeAction => chaincodeAction.getResults_asU8()) )
.map(bytes => ledger.rwset.TxReadWriteSet.deserializeBinary(bytes)) )
.map((endorsedAction) =>
endorsedAction.getProposalResponsePayload_asU8()
)
.map((bytes) =>
peer.ProposalResponsePayload.deserializeBinary(bytes)
)
.map((responsePayload) =>
peer.ChaincodeAction.deserializeBinary(
responsePayload.getExtension_asU8()
)
)
.map((chaincodeAction) => chaincodeAction.getResults_asU8())
.map((bytes) =>
ledger.rwset.TxReadWriteSet.deserializeBinary(bytes)
)
.map(parseReadWriteSet) .map(parseReadWriteSet)
), ),
toProto: () => transaction, toProto: () => transaction,
@ -109,67 +142,88 @@ function newTransaction(payload: Payload): Transaction {
getChannelHeader: () => payload.getChannelHeader(), getChannelHeader: () => payload.getChannelHeader(),
getCreator: () => { getCreator: () => {
const creatorBytes = payload.getSignatureHeader().getCreator_asU8(); const creatorBytes = payload.getSignatureHeader().getCreator_asU8();
const creator = msp.SerializedIdentity.deserializeBinary(creatorBytes); const creator =
msp.SerializedIdentity.deserializeBinary(creatorBytes);
return { return {
mspId: creator.getMspid(), mspId: creator.getMspid(),
credentials: creator.getIdBytes_asU8(), credentials: creator.getIdBytes_asU8(),
}; };
}, },
getNamespaceReadWriteSets: () => transaction.getReadWriteSets() getNamespaceReadWriteSets: () =>
.flatMap(readWriteSet => readWriteSet.getNamespaceReadWriteSets()), transaction
.getReadWriteSets()
.flatMap((readWriteSet) =>
readWriteSet.getNamespaceReadWriteSets()
),
getValidationCode: () => payload.getTransactionValidationCode(), getValidationCode: () => payload.getTransactionValidationCode(),
isValid: () => payload.isValid(), isValid: () => payload.isValid(),
toProto: () => payload.toProto(), toProto: () => payload.toProto(),
}; };
} }
function parseReadWriteSet(readWriteSet: ledger.rwset.TxReadWriteSet): ReadWriteSet { function parseReadWriteSet(
readWriteSet: ledger.rwset.TxReadWriteSet
): ReadWriteSet {
return { return {
getNamespaceReadWriteSets: () => { getNamespaceReadWriteSets: () =>
if (readWriteSet.getDataModel() !== ledger.rwset.TxReadWriteSet.DataModel.KV) { readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet),
throw new Error(`Unexpected read/write set data model: ${readWriteSet.getDataModel()}`);
}
return readWriteSet.getNsRwsetList().map(parseNamespaceReadWriteSet);
},
toProto: () => readWriteSet, toProto: () => readWriteSet,
}; };
} }
function parseNamespaceReadWriteSet(nsReadWriteSet: ledger.rwset.NsReadWriteSet): NamespaceReadWriteSet { function parseNamespaceReadWriteSet(
nsReadWriteSet: ledger.rwset.NsReadWriteSet
): NamespaceReadWriteSet {
return { return {
getNamespace: () => nsReadWriteSet.getNamespace(), getNamespace: () => nsReadWriteSet.getNamespace(),
getReadWriteSet: cache( getReadWriteSet: cache(() =>
() => ledger.rwset.kvrwset.KVRWSet.deserializeBinary(nsReadWriteSet.getRwset_asU8()) ledger.rwset.kvrwset.KVRWSet.deserializeBinary(
nsReadWriteSet.getRwset_asU8()
)
), ),
toProto: () => nsReadWriteSet, toProto: () => nsReadWriteSet,
}; };
} }
function getTransactionValidationCodes(block: common.Block): Uint8Array { function getTransactionValidationCodes(block: common.Block): Uint8Array {
const metadata = assertDefined(block.getMetadata(), 'Missing block metadata'); const metadata = assertDefined(
return metadata.getMetadataList_asU8()[common.BlockMetadataIndex.TRANSACTIONS_FILTER]; block.getMetadata(),
'Missing block metadata'
);
return assertDefined(
metadata.getMetadataList_asU8()[
common.BlockMetadataIndex.TRANSACTIONS_FILTER
],
'Missing transaction validation code'
);
} }
function getPayloads(block: common.Block): common.Payload[] { function getPayloads(block: common.Block): common.Payload[] {
return (block.getData()?.getDataList_asU8() ?? []) return (block.getData()?.getDataList_asU8() ?? [])
.map(bytes => common.Envelope.deserializeBinary(bytes)) .map((bytes) => common.Envelope.deserializeBinary(bytes))
.map(envelope => envelope.getPayload_asU8()) .map((envelope) => envelope.getPayload_asU8())
.map(bytes => common.Payload.deserializeBinary(bytes)); .map((bytes) => common.Payload.deserializeBinary(bytes));
} }
function getChannelHeader(payload: common.Payload): common.ChannelHeader { function getChannelHeader(payload: common.Payload): common.ChannelHeader {
const header = assertDefined(payload.getHeader(), 'Missing payload header'); const header = assertDefined(payload.getHeader(), 'Missing payload header');
return common.ChannelHeader.deserializeBinary(header.getChannelHeader_asU8()); return common.ChannelHeader.deserializeBinary(
header.getChannelHeader_asU8()
);
} }
function getSignatureHeader(payload: common.Payload): common.SignatureHeader { function getSignatureHeader(payload: common.Payload): common.SignatureHeader {
const header = assertDefined(payload.getHeader(), 'Missing payload header'); const header = assertDefined(payload.getHeader(), 'Missing payload header');
return common.SignatureHeader.deserializeBinary(header.getSignatureHeader_asU8()); return common.SignatureHeader.deserializeBinary(
header.getSignatureHeader_asU8()
);
} }
function getChaincodeActionPayloads(transaction: peer.Transaction): peer.ChaincodeActionPayload[] { function getChaincodeActionPayloads(
return transaction.getActionsList() transaction: peer.Transaction
.map(transactionAction => transactionAction.getPayload_asU8()) ): peer.ChaincodeActionPayload[] {
.map(bytes => peer.ChaincodeActionPayload.deserializeBinary(bytes)); return transaction
.getActionsList()
.map((transactionAction) => transactionAction.getPayload_asU8())
.map((bytes) => peer.ChaincodeActionPayload.deserializeBinary(bytes));
} }

View file

@ -70,10 +70,11 @@ async function newIdentity(): Promise<Identity> {
async function newSigner(): Promise<Signer> { async function newSigner(): Promise<Signer> {
const keyFiles = await fs.readdir(keyDirectoryPath); const keyFiles = await fs.readdir(keyDirectoryPath);
if (keyFiles.length === 0) { const keyFile = keyFiles[0];
if (!keyFile) {
throw new Error(`No private key files found in directory ${keyDirectoryPath}`); throw new Error(`No private key files found in directory ${keyDirectoryPath}`);
} }
const keyPath = path.resolve(keyDirectoryPath, keyFiles[0]); const keyPath = path.resolve(keyDirectoryPath, keyFile);
const privateKeyPem = await fs.readFile(keyPath); const privateKeyPem = await fs.readFile(keyPath);
const privateKey = crypto.createPrivateKey(privateKeyPem); const privateKey = crypto.createPrivateKey(privateKeyPem);
return signers.newPrivateKeySigner(privateKey); return signers.newPrivateKeySigner(privateKey);

View file

@ -20,7 +20,10 @@ export async function main(client: Client): Promise<void> {
const smartContract = new AssetTransferBasic(contract); const smartContract = new AssetTransferBasic(contract);
const assets = await smartContract.getAllAssets(); const assets = await smartContract.getAllAssets();
const assetsJson = JSON.stringify(assets, undefined, 2); const assetsJson = JSON.stringify(assets, undefined, 2);
assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation // Write line-by-line to avoid truncation
assetsJson.split('\n').forEach((line) => {
console.log(line);
});
} finally { } finally {
gateway.close(); gateway.close();
} }

View file

@ -87,10 +87,10 @@ export async function main(client: Client): Promise<void> {
const network = gateway.getNetwork(channelName); const network = gateway.getNetwork(channelName);
const checkpointer = await checkpointers.file(checkpointFile); const checkpointer = await checkpointers.file(checkpointFile);
console.log(`Starting event listening from block ${checkpointer.getBlockNumber() ?? startBlock}`); console.log('Starting event listening from block', checkpointer.getBlockNumber() ?? startBlock);
console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); console.log('Last processed transaction ID within block:', checkpointer.getTransactionId());
if (simulatedFailureCount > 0) { if (simulatedFailureCount > 0) {
console.log(`Simulating a write failure every ${simulatedFailureCount} transactions`); console.log('Simulating a write failure every', simulatedFailureCount, 'transactions');
} }
const blocks = await network.getBlockEvents({ const blocks = await network.getBlockEvents({
@ -135,7 +135,7 @@ class BlockProcessor {
async process(): Promise<void> { async process(): Promise<void> {
const blockNumber = this.#block.getNumber(); const blockNumber = this.#block.getNumber();
console.log(`\nReceived block ${blockNumber}`); console.log(`\nReceived block ${String(blockNumber)}`);
const validTransactions = this.#getNewTransactions() const validTransactions = this.#getNewTransactions()
.filter(transaction => transaction.isValid()); .filter(transaction => transaction.isValid());
@ -168,7 +168,7 @@ class BlockProcessor {
const blockTransactionIds = transactions.map(transaction => transaction.getChannelHeader().getTxId()); const blockTransactionIds = transactions.map(transaction => transaction.getChannelHeader().getTxId());
const lastProcessedIndex = blockTransactionIds.indexOf(lastTransactionId); const lastProcessedIndex = blockTransactionIds.indexOf(lastTransactionId);
if (lastProcessedIndex < 0) { if (lastProcessedIndex < 0) {
throw new Error(`Checkpoint transaction ID ${lastTransactionId} not found in block ${this.#block.getNumber()} containing transactions: ${blockTransactionIds.join(', ')}`); throw new Error(`Checkpoint transaction ID ${lastTransactionId} not found in block ${String(this.#block.getNumber())} containing transactions: ${blockTransactionIds.join(', ')}`);
} }
return transactions.slice(lastProcessedIndex + 1); return transactions.slice(lastProcessedIndex + 1);

View file

@ -9,7 +9,8 @@
* @param values Candidate elements. * @param values Candidate elements.
*/ */
export function randomElement<T>(values: T[]): T { export function randomElement<T>(values: T[]): T {
return values[randomInt(values.length)]; const result = values[randomInt(values.length)];
return assertDefined(result, `Missing element in {String(values)}`);
} }
/** /**
@ -42,7 +43,7 @@ export async function allFulfilled(promises: Promise<unknown>[]): Promise<void>
if (failures.length > 0) { if (failures.length > 0) {
const failMessages = ' - ' + failures.join('\n - '); const failMessages = ' - ' + failures.join('\n - ');
throw new Error(`${failures.length} failures:\n${failMessages}\n`); throw new Error(`${String(failures.length)} failures:\n${failMessages}\n`);
} }
} }

View file

@ -1,19 +1,15 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitReturns": true "noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["./src/**/*"],
"src/" "exclude": ["./src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }