mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
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:
parent
a4f0a2c5b2
commit
c077dae79c
87 changed files with 6194 additions and 4254 deletions
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
asset-transfer-basic/rest-api-typescript/.editorconfig
Normal file
3
asset-transfer-basic/rest-api-typescript/.editorconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[*.ts]
|
||||||
|
indent_size = 4
|
||||||
|
quote_type = single
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
|
|
@ -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',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
asset-transfer-events/application-gateway-typescript/tsconfig.json
Executable file → Normal file
28
asset-transfer-events/application-gateway-typescript/tsconfig.json
Executable file → Normal 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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
2274
full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json
generated
Normal file
2274
full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
13
off_chain_data/application-typescript/eslint.config.mjs
Normal file
13
off_chain_data/application-typescript/eslint.config.mjs
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue