Update Node chaincode for v2.5.5 release

Signed-off-by: Mark S. Lewis <Mark.S.Lewis@outlook.com>
This commit is contained in:
Mark S. Lewis 2024-05-31 19:22:30 +01:00 committed by Dave Enyeart
parent 3c63eac4e3
commit 0ed34585e1
33 changed files with 15507 additions and 10269 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,7 @@
"description": "Asset-Transfer-Basic contract implemented in JavaScript", "description": "Asset-Transfer-Basic contract implemented in JavaScript",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
"node": ">=12", "node": ">=18"
"npm": ">=5"
}, },
"scripts": { "scripts": {
"lint": "eslint *.js */**.js", "lint": "eslint *.js */**.js",
@ -17,18 +16,18 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.0.0", "fabric-contract-api": "~2.5.5",
"fabric-shim": "^2.0.0", "fabric-shim": "~2.5.5",
"json-stringify-deterministic": "^1.0.1", "json-stringify-deterministic": "^1.0.0",
"sort-keys-recursive": "^2.1.2" "sort-keys-recursive": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "chai": "^4.4.1",
"eslint": "^4.19.1", "eslint": "^8.57.0",
"mocha": "^8.0.1", "mocha": "^10.4.0",
"nyc": "^14.1.1", "nyc": "^15.1.0",
"sinon": "^6.0.0", "sinon": "^18.0.0",
"sinon-chai": "^3.2.0" "sinon-chai": "^3.7.0"
}, },
"nyc": { "nyc": {
"exclude": [ "exclude": [

View file

@ -227,10 +227,10 @@ describe('Asset Transfer Basic Tests', () => {
expect(ret.length).to.equal(4); expect(ret.length).to.equal(4);
let expected = [ let expected = [
{Record: {ID: 'asset1', Color: 'blue', Size: 5, Owner: 'Robert', AppraisedValue: 100}}, {ID: 'asset1', Color: 'blue', Size: 5, Owner: 'Robert', AppraisedValue: 100},
{Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200},
{Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300},
{Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}
]; ];
expect(ret).to.eql(expected); expect(ret).to.eql(expected);
@ -256,10 +256,10 @@ describe('Asset Transfer Basic Tests', () => {
expect(ret.length).to.equal(4); expect(ret.length).to.equal(4);
let expected = [ let expected = [
{Record: 'non-json-value'}, 'non-json-value',
{Record: {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200}}, {ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200},
{Record: {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300}}, {ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300},
{Record: {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}} {ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}
]; ];
expect(ret).to.eql(expected); expect(ret).to.eql(expected);

View file

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

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,12 @@
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"engines": { "engines": {
"node": ">=12", "node": ">=18"
"npm": ">=5"
}, },
"scripts": { "scripts": {
"lint": "tslint -c tslint.json 'src/**/*.ts'", "lint": "eslint src",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "nyc mocha -r ts-node/register src/**/*.spec.ts", "test": "",
"start": "set -x && fabric-chaincode-node start", "start": "set -x && fabric-chaincode-node start",
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
@ -26,44 +25,17 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.4.0", "fabric-contract-api": "~2.5.5",
"fabric-shim": "^2.4.0", "fabric-shim": "~2.5.5",
"json-stringify-deterministic": "^1.0.1", "json-stringify-deterministic": "^1.0.0",
"sort-keys-recursive": "^2.1.2" "sort-keys-recursive": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.7", "@types/node": "^18.19.33",
"@types/mocha": "^5.2.5", "@eslint/js": "^9.3.0",
"@types/node": "^12.20.55", "@tsconfig/node18": "^18.2.4",
"@types/sinon": "^5.0.7", "eslint": "^8.57.0",
"@types/sinon-chai": "^3.2.1", "typescript": "~5.4.5",
"chai": "^4.2.0", "typescript-eslint": "^7.11.0"
"mocha": "^10.0.0",
"nyc": "^14.1.1",
"sinon": "^7.1.1",
"sinon-chai": "^3.3.0",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^4.4"
},
"nyc": {
"extension": [
".ts",
".tsx"
],
"exclude": [
"coverage/**",
"dist/**"
],
"reporter": [
"text-summary",
"html"
],
"all": true,
"check-coverage": true,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
} }
} }

View file

@ -91,7 +91,7 @@ export class AssetTransferContract extends Contract {
@Transaction(false) @Transaction(false)
public async ReadAsset(ctx: Context, id: string): Promise<string> { public async ReadAsset(ctx: Context, id: string): Promise<string> {
const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state
if (!assetJSON || assetJSON.length === 0) { if (assetJSON.length === 0) {
throw new Error(`The asset ${id} does not exist`); throw new Error(`The asset ${id} does not exist`);
} }
return assetJSON.toString(); return assetJSON.toString();
@ -132,14 +132,14 @@ export class AssetTransferContract extends Contract {
@Returns('boolean') @Returns('boolean')
public async AssetExists(ctx: Context, id: string): Promise<boolean> { public async AssetExists(ctx: Context, id: string): Promise<boolean> {
const assetJSON = await ctx.stub.getState(id); const assetJSON = await ctx.stub.getState(id);
return assetJSON && assetJSON.length > 0; return assetJSON.length > 0;
} }
// TransferAsset updates the owner field of asset with given id in the world state, and returns the old owner. // TransferAsset updates the owner field of asset with given id in the world state, and returns the old owner.
@Transaction() @Transaction()
public async TransferAsset(ctx: Context, id: string, newOwner: string): Promise<string> { public async TransferAsset(ctx: Context, id: string, newOwner: string): Promise<string> {
const assetString = await this.ReadAsset(ctx, id); const assetString = await this.ReadAsset(ctx, id);
const asset = JSON.parse(assetString); const asset = JSON.parse(assetString) as Asset;
const oldOwner = asset.Owner; const oldOwner = asset.Owner;
asset.Owner = newOwner; asset.Owner = newOwner;
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive' // we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
@ -159,7 +159,7 @@ export class AssetTransferContract extends Contract {
const strValue = Buffer.from(result.value.value.toString()).toString('utf8'); const strValue = Buffer.from(result.value.value.toString()).toString('utf8');
let record; let record;
try { try {
record = JSON.parse(strValue); record = JSON.parse(strValue) as Asset;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
record = strValue; record = strValue;

View file

@ -2,8 +2,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import {type Contract} from 'fabric-contract-api';
import {AssetTransferContract} from './assetTransfer'; import {AssetTransferContract} from './assetTransfer';
export {AssetTransferContract} from './assetTransfer'; export const contracts: typeof Contract[] = [AssetTransferContract];
export const contracts: any[] = [AssetTransferContract];

View file

@ -1,19 +1,17 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"outDir": "dist",
"target": "es2017",
"moduleResolution": "node",
"module": "commonjs",
"esModuleInterop": true,
"declaration": true, "declaration": true,
"sourceMap": true "declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/"]
"./src/**/*"
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -1,23 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"indent": [true, "spaces", 4],
"linebreak-style": [true, "LF"],
"quotemark": [true, "single"],
"semicolon": [true, "always"],
"no-console": false,
"curly": true,
"triple-equals": true,
"no-string-throw": true,
"no-var-keyword": true,
"no-trailing-whitespace": true,
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,
"max-line-length": false
},
"rulesDirectory": []
}

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,7 @@
"description": "Asset-Transfer-Events contract implemented in JavaScript", "description": "Asset-Transfer-Events contract implemented in JavaScript",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
"node": ">=12", "node": ">=18"
"npm": ">=5"
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
@ -17,16 +16,16 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.0.0", "fabric-contract-api": "~2.5.5",
"fabric-shim": "^2.0.0" "fabric-shim": "~2.5.5"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "chai": "^4.4.1",
"eslint": "^4.19.1", "eslint": "^8.57.0",
"mocha": "^8.0.1", "mocha": "^10.4.0",
"nyc": "^14.1.1", "nyc": "^15.1.0",
"sinon": "^6.0.0", "sinon": "^18.0.0",
"sinon-chai": "^3.2.0" "sinon-chai": "^3.7.0"
}, },
"nyc": { "nyc": {
"exclude": [ "exclude": [

View file

@ -84,9 +84,8 @@ describe('Asset Transfer Events Tests', () => {
Price: '90', Price: '90',
salt: Buffer.from(randomNumber.toString()).toString('hex') salt: Buffer.from(randomNumber.toString()).toString('hex')
}; };
transientMap = { transientMap = new Map();
asset_properties: Buffer.from(JSON.stringify(asset_properties)) transientMap.set('asset_properties', Buffer.from(JSON.stringify(asset_properties)));
};
}); });
describe('Test CreateAsset', () => { describe('Test CreateAsset', () => {

File diff suppressed because it is too large Load diff

View file

@ -4,20 +4,20 @@
"description": "asset chaincode implemented in node.js", "description": "asset chaincode implemented in node.js",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
"node": ">=12", "node": ">=18"
"npm": ">=5.3.0"
}, },
"scripts": { "scripts": {
"start": "fabric-chaincode-node start", "start": "fabric-chaincode-node start",
"lint": "eslint *.js */**.js" "lint": "eslint *.js */**.js",
"test": ""
}, },
"engine-strict": true, "engine-strict": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.0.0", "fabric-contract-api": "~2.5.5",
"fabric-shim": "^2.0.0" "fabric-shim": "~2.5.5"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.32.0" "eslint": "^8.57.0"
} }
} }

View file

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

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,12 @@
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"engines": { "engines": {
"node": ">=12", "node": ">=18"
"npm": ">=5"
}, },
"scripts": { "scripts": {
"lint": "tslint -c tslint.json 'src/**/*.ts'", "lint": "eslint src",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "nyc mocha -r ts-node/register src/**/*.spec.ts", "test": "",
"start": "set -x && fabric-chaincode-node start", "start": "set -x && fabric-chaincode-node start",
"build": "tsc", "build": "tsc",
"build:watch": "tsc -w", "build:watch": "tsc -w",
@ -26,44 +25,17 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "^2.4.0", "fabric-contract-api": "~2.5.5",
"fabric-shim": "^2.4.0", "fabric-shim": "~2.5.5",
"json-stringify-deterministic": "^1.0.1", "json-stringify-deterministic": "^1.0.0",
"sort-keys-recursive": "^2.1.2" "sort-keys-recursive": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.7", "@types/node": "^18.19.33",
"@types/mocha": "^5.2.5", "@eslint/js": "^9.3.0",
"@types/node": "^12.20.55", "@tsconfig/node18": "^18.2.4",
"@types/sinon": "^5.0.7", "eslint": "^8.57.0",
"@types/sinon-chai": "^3.2.1", "typescript": "~5.4.5",
"chai": "^4.2.0", "typescript-eslint": "^7.11.0"
"mocha": "^10.0.0",
"nyc": "^14.1.1",
"sinon": "^7.1.1",
"sinon-chai": "^3.3.0",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.1.6"
},
"nyc": {
"extension": [
".ts",
".tsx"
],
"exclude": [
"coverage/**",
"dist/**"
],
"reporter": [
"text-summary",
"html"
],
"all": true,
"check-coverage": true,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
} }
} }

View file

@ -2,23 +2,41 @@
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
*/ */
import { Object, Property } from 'fabric-contract-api'; import { Object, Property } from "fabric-contract-api";
import { nonEmptyString, positiveNumber } from "./utils";
@Object() @Object()
// Asset describes main asset details that are visible to all organizations // Asset describes main asset details that are visible to all organizations
export class Asset { export class Asset {
@Property() @Property()
public docType?: string; docType?: string;
@Property() @Property()
public ID: string; ID: string = "";
@Property() @Property()
public Color: string; Color: string = "";
@Property() @Property()
public Size: number; Size: number = 0;
@Property() @Property()
public Owner: string; Owner: string = "";
static fromBytes(bytes: Uint8Array): Asset {
if (bytes.length === 0) {
throw new Error("no asset data");
}
const json = Buffer.from(bytes).toString();
const properties = JSON.parse(json) as Partial<Asset>;
const result = new Asset();
result.docType = properties.docType;
result.ID = nonEmptyString(properties.ID, "ID field must be a non-empty string");
result.Color = nonEmptyString(properties.Color, "Color field must be a non-empty string");
result.Size = positiveNumber(properties.Size, "Size field must be a positive integer");
result.Owner = nonEmptyString(properties.Owner, "appraiseOwner field must be a non-empty string");
return result;
}
} }

View file

@ -6,7 +6,7 @@ import stringify from 'json-stringify-deterministic';
import sortKeysRecursive from 'sort-keys-recursive'; import sortKeysRecursive from 'sort-keys-recursive';
import { Asset } from './asset'; import { Asset } from './asset';
import { AssetPrivateDetails } from './assetTransferDetails'; import { AssetPrivateDetails } from './assetTransferDetails';
import { AssetTransferTransientInput } from './assetTransferTransientInput'; import { TransientAssetDelete, TransientAssetOwner, TransientAssetProperties, TransientAssetPurge, TransientAssetValue } from './assetTransferTransientInput';
import { TransferAgreement } from './transferAgreement'; import { TransferAgreement } from './transferAgreement';
const assetCollection = 'assetCollection'; const assetCollection = 'assetCollection';
@ -19,35 +19,12 @@ export class AssetTransfer extends Contract {
@Transaction() @Transaction()
public async CreateAsset(ctx: Context): Promise<void> { public async CreateAsset(ctx: Context): Promise<void> {
const transientMap = ctx.stub.getTransient(); const transientMap = ctx.stub.getTransient();
const transientAssetJSON = transientMap.get('asset_properties'); const assetProperties = new TransientAssetProperties(transientMap);
if (transientAssetJSON.length === 0) {
throw new Error('asset properties not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...transientAssetJSON);
const jsonFromString = JSON.parse(jsonBytesToString);
// Check properties
if (jsonFromString.objectType.length === 0) {
throw new Error('objectType field must be a non-empty string');
}
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
if (jsonFromString.color.length === 0) {
throw new Error('color field must be a non-empty string');
}
if (jsonFromString.size <= 0) {
throw new Error('size field must be a positive integer');
}
if (jsonFromString.appraisedValue <= 0) {
throw new Error('appraisedValue field must be a positive integer');
}
// Check if asset already exists // Check if asset already exists
const assetAsBytes = await ctx.stub.getPrivateData(assetCollection, jsonFromString.assetID); const assetAsBytes = await ctx.stub.getPrivateData(assetCollection, assetProperties.assetID);
if (assetAsBytes.length !== 0) { if (assetAsBytes.length !== 0) {
throw new Error('this asset already exists: ' + jsonFromString.assetID); throw new Error('this asset already exists: ' + assetProperties.assetID);
} }
// Get ID of submitting client identity // Get ID of submitting client identity
@ -56,14 +33,12 @@ export class AssetTransfer extends Contract {
// Verify that the client is submitting request to peer in their organization // Verify that the client is submitting request to peer in their organization
// This is to ensure that a client from another org doesn't attempt to read or // This is to ensure that a client from another org doesn't attempt to read or
// write private data from this peer. // write private data from this peer.
const err = await this.verifyClientOrgMatchesPeerOrg(ctx); this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('CreateAsset cannot be performed: Error ' + err);
}
const asset: Asset = { const asset: Asset = {
ID: jsonFromString.assetID, ID: assetProperties.assetID,
Color: jsonFromString.color, Color: assetProperties.color,
Size: jsonFromString.size, Size: assetProperties.size,
Owner: clientID, Owner: clientID,
}; };
@ -74,16 +49,16 @@ export class AssetTransfer extends Contract {
// Save asset details to collection visible to owning organization // Save asset details to collection visible to owning organization
const assetPrivateDetails: AssetPrivateDetails = { const assetPrivateDetails: AssetPrivateDetails = {
ID: jsonFromString.assetID, ID: assetProperties.assetID,
AppraisedValue: jsonFromString.appraisedValue, AppraisedValue: assetProperties.appraisedValue,
}; };
// Get collection name for this organization. // Get collection name for this organization.
const orgCollection = await this.getCollectionName(ctx); const orgCollection = this.getCollectionName(ctx);
// Put asset appraised value into owners org specific private data collection // Put asset appraised value into owners org specific private data collection
console.log('Put: collection %v, ID %v', orgCollection, jsonFromString.assetID); console.log('Put: collection %v, ID %v', orgCollection, assetProperties.assetID);
await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(assetPrivateDetails)))); await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(assetPrivateDetails))));
return null;
} }
// AgreeToTransfer is used by the potential buyer of the asset to agree to the // AgreeToTransfer is used by the potential buyer of the asset to agree to the
// asset value. The agreed to appraisal value is stored in the buying orgs // asset value. The agreed to appraisal value is stored in the buying orgs
// org specifc collection, while the the buyer client ID is stored in the asset collection // org specifc collection, while the the buyer client ID is stored in the asset collection
@ -94,33 +69,19 @@ export class AssetTransfer extends Contract {
const clientID = ctx.clientIdentity.getID(); const clientID = ctx.clientIdentity.getID();
// Value is private, therefore it gets passed in transient field // Value is private, therefore it gets passed in transient field
const transientMap = ctx.stub.getTransient(); const transientMap = ctx.stub.getTransient();
// Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. const assetValue = new TransientAssetValue(transientMap);
const valueJSONasBytes = transientMap.get('asset_value');
if (valueJSONasBytes.length === 0) {
throw new Error('asset value not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...valueJSONasBytes);
const jsonFromString = JSON.parse(jsonBytesToString);
// Do some error checking since we get the chance
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
if (jsonFromString.appraisedValue <= 0) {
throw new Error('appraisedValue field must be a positive integer');
}
const valueJSON: AssetPrivateDetails = { const valueJSON: AssetPrivateDetails = {
ID: jsonFromString.assetID, ID: assetValue.assetID,
AppraisedValue: jsonFromString.appraisedValue, AppraisedValue: assetValue.appraisedValue,
}; };
// Read asset from the private data collection // Read asset from the private data collection
const asset = await this.ReadAsset(ctx, valueJSON.ID); const asset = await this.ReadAsset(ctx, valueJSON.ID);
// Verify that the client is submitting request to peer in their organization // Verify that the client is submitting request to peer in their organization
const err = await this.verifyClientOrgMatchesPeerOrg(ctx); this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('AgreeToTransfer cannot be performed: Error ' + err);
}
// Get collection name for this organization. Needs to be read by a member of the organization. // Get collection name for this organization. Needs to be read by a member of the organization.
const orgCollection = await this.getCollectionName(ctx); const orgCollection = this.getCollectionName(ctx);
console.log(`AgreeToTransfer Put: collection ${orgCollection}, ID ${valueJSON.ID}`); console.log(`AgreeToTransfer Put: collection ${orgCollection}, ID ${valueJSON.ID}`);
// Put agreed value in the org specifc private data collection // Put agreed value in the org specifc private data collection
await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(valueJSON)))); await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(valueJSON))));
@ -130,170 +91,123 @@ export class AssetTransfer extends Contract {
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [valueJSON.ID]); const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [valueJSON.ID]);
console.log(`AgreeToTransfer Put: collection ${assetCollection}, ID ${valueJSON.ID}, Key ${transferAgreeKey}`); console.log(`AgreeToTransfer Put: collection ${assetCollection}, ID ${valueJSON.ID}, Key ${transferAgreeKey}`);
await ctx.stub.putPrivateData(assetCollection, transferAgreeKey, new Uint8Array(Buffer.from(clientID))); await ctx.stub.putPrivateData(assetCollection, transferAgreeKey, new Uint8Array(Buffer.from(clientID)));
return null;
} }
@Transaction() @Transaction()
// TransferAsset transfers the asset to the new owner by setting a new owner ID // TransferAsset transfers the asset to the new owner by setting a new owner ID
public async TransferAsset(ctx: Context): Promise<void> { public async TransferAsset(ctx: Context): Promise<void> {
const transientMap = ctx.stub.getTransient();
// Asset properties are private, therefore they get passed in transient field // Asset properties are private, therefore they get passed in transient field
const transientTransferJSON = transientMap.get('asset_owner'); const transientMap = ctx.stub.getTransient();
if (transientTransferJSON.length === 0) { const assetOwner = new TransientAssetOwner(transientMap);
throw new Error('asset owner not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...transientTransferJSON);
const jsonFromString = JSON.parse(jsonBytesToString);
// Do some error checking since we get the chance
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
if (jsonFromString.buyerMSP.length === 0) {
throw new Error('buyerMSP field must be a non-empty string');
}
const assetTransferInput: AssetTransferTransientInput = {
ID: jsonFromString.assetID,
BuyerMSP: jsonFromString.buyerMSP,
};
console.log('TransferAsset: verify asset exists ID ' + assetTransferInput.ID);
// Read asset from the private data collection
const asset = await this.ReadAsset(ctx, assetTransferInput.ID);
// Verify that the client is submitting request to peer in their organization
const err = await this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('TransferAsset cannot be performed: Error ' + err);
}
// Verify transfer details and transfer owner
await this.verifyAgreement(ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP);
const transferAgreement = await this.ReadTransferAgreement(ctx, assetTransferInput.ID); console.log('TransferAsset: verify asset exists ID ' + assetOwner.assetID);
// Read asset from the private data collection
const asset = await this.ReadAsset(ctx, assetOwner.assetID);
// Verify that the client is submitting request to peer in their organization
this.verifyClientOrgMatchesPeerOrg(ctx);
// Verify transfer details and transfer owner
await this.verifyAgreement(ctx, assetOwner.assetID, asset.Owner, assetOwner.buyerMSP);
const transferAgreement = await this.ReadTransferAgreement(ctx, assetOwner.assetID);
if (transferAgreement.BuyerID === '') { if (transferAgreement.BuyerID === '') {
throw new Error('BuyerID not found in TransferAgreement for ' + assetTransferInput.ID); throw new Error('BuyerID not found in TransferAgreement for ' + assetOwner.assetID);
} }
// Transfer asset in private data collection to new owner // Transfer asset in private data collection to new owner
asset.Owner = transferAgreement.BuyerID; asset.Owner = transferAgreement.BuyerID;
console.log(`TransferAsset Put: collection ${assetCollection}, ID ${assetTransferInput.ID}`); console.log(`TransferAsset Put: collection ${assetCollection}, ID ${assetOwner.assetID}`);
await ctx.stub.putPrivateData(assetCollection, assetTransferInput.ID, Buffer.from(stringify(sortKeysRecursive(asset)))); // rewrite the asset await ctx.stub.putPrivateData(assetCollection, assetOwner.assetID, Buffer.from(stringify(sortKeysRecursive(asset)))); // rewrite the asset
// Get collection name for this organization // Get collection name for this organization
const ownersCollection = await this.getCollectionName(ctx); const ownersCollection = this.getCollectionName(ctx);
// Delete the asset appraised value from this organization's private data collection // Delete the asset appraised value from this organization's private data collection
await ctx.stub.deletePrivateData(ownersCollection, assetTransferInput.ID); await ctx.stub.deletePrivateData(ownersCollection, assetOwner.assetID);
// Delete the transfer agreement from the asset collection // Delete the transfer agreement from the asset collection
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [assetTransferInput.ID]); const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [assetOwner.assetID]);
await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey); await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey);
return null;
} }
@Transaction() @Transaction()
// DeleteAsset can be used by the owner of the asset to delete the asset // DeleteAsset can be used by the owner of the asset to delete the asset
public async DeleteAsset(ctx: Context): Promise<void> { public async DeleteAsset(ctx: Context): Promise<void> {
// Value is private, therefore it gets passed in transient field // Value is private, therefore it gets passed in transient field
const transientMap = ctx.stub.getTransient(); const transientMap = ctx.stub.getTransient();
// Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. const assetDelete = new TransientAssetDelete(transientMap);
const valueJSONasBytes = transientMap.get('asset_delete');
if (valueJSONasBytes.length === 0) {
throw new Error('asset to delete not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...valueJSONasBytes);
const jsonFromString = JSON.parse(jsonBytesToString);
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
// Verify that the client is submitting request to peer in their organization // Verify that the client is submitting request to peer in their organization
const err = await this.verifyClientOrgMatchesPeerOrg(ctx); this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('DeleteAsset cannot be performed: Error ' + err); console.log('Deleting Asset: ' + assetDelete.assetID);
}
console.log('Deleting Asset: ' + jsonFromString.assetID);
// get the asset from chaincode state // get the asset from chaincode state
const valAsbytes = await ctx.stub.getPrivateData(assetCollection, jsonFromString.assetID); const valAsbytes = await ctx.stub.getPrivateData(assetCollection, assetDelete.assetID);
if (valAsbytes.length === 0) { if (valAsbytes.length === 0) {
throw new Error('asset not found: ' + jsonFromString.assetID); throw new Error('asset not found: ' + assetDelete.assetID);
} }
const ownerCollection = await this.getCollectionName(ctx); const ownerCollection = this.getCollectionName(ctx);
// Check the asset is in the caller org's private collection // Check the asset is in the caller org's private collection
const valAsbytesPrivate = await ctx.stub.getPrivateData(ownerCollection, jsonFromString.assetID); const valAsbytesPrivate = await ctx.stub.getPrivateData(ownerCollection, assetDelete.assetID);
if (valAsbytesPrivate.length === 0) { if (valAsbytesPrivate.length === 0) {
throw new Error(`asset not found in owner's private Collection: ${ownerCollection} : ${jsonFromString.assetID}`); throw new Error(`asset not found in owner's private Collection: ${ownerCollection} : ${assetDelete.assetID}`);
} }
// delete the asset from state // delete the asset from state
await ctx.stub.deletePrivateData(assetCollection, jsonFromString.assetID); await ctx.stub.deletePrivateData(assetCollection, assetDelete.assetID);
// Finally, delete private details of asset // Finally, delete private details of asset
await ctx.stub.deletePrivateData(ownerCollection, jsonFromString.assetID); await ctx.stub.deletePrivateData(ownerCollection, assetDelete.assetID);
return null;
} }
@Transaction() @Transaction()
// PurgeAsset can be used by the owner of the asset to delete the asset // PurgeAsset can be used by the owner of the asset to delete the asset
// Trigger removal of the asset // Trigger removal of the asset
public async PurgeAsset(ctx: Context): Promise<void> { public async PurgeAsset(ctx: Context): Promise<void> {
// Value is private, therefore it gets passed in transient field // Value is private, therefore it gets passed in transient field
const transientMap = ctx.stub.getTransient(); const transientMap = ctx.stub.getTransient();
// Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. const assetPurge = new TransientAssetPurge(transientMap);
const valueJSONasBytes = transientMap.get('asset_purge');
if (valueJSONasBytes.length === 0) {
throw new Error('asset to purge not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...valueJSONasBytes);
const jsonFromString = JSON.parse(jsonBytesToString);
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
// Verify that the client is submitting request to peer in their organization // Verify that the client is submitting request to peer in their organization
const err = await this.verifyClientOrgMatchesPeerOrg(ctx); this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('PurgeAsset cannot be performed: Error ' + err); console.log('Purging Asset: ' + assetPurge.assetID);
}
console.log('Purging Asset: ' + jsonFromString.assetID);
// Note that there is no check here to see if the id exist; it might have been 'deleted' already // Note that there is no check here to see if the id exist; it might have been 'deleted' already
// so a check here is pointless. We would need to call purge irrespective of the result // so a check here is pointless. We would need to call purge irrespective of the result
// A delete can be called before purge, but is not essential // A delete can be called before purge, but is not essential
const ownerCollection = await this.getCollectionName(ctx); const ownerCollection = this.getCollectionName(ctx);
// delete the asset from state // delete the asset from state
await ctx.stub.purgePrivateData(assetCollection, jsonFromString.assetID); await ctx.stub.purgePrivateData(assetCollection, assetPurge.assetID);
// Finally, delete private details of asset // Finally, delete private details of asset
await ctx.stub.purgePrivateData(ownerCollection, jsonFromString.assetID); await ctx.stub.purgePrivateData(ownerCollection, assetPurge.assetID);
return null;
} }
@Transaction() @Transaction()
// DeleteTranferAgreement can be used by the buyer to withdraw a proposal from // DeleteTranferAgreement can be used by the buyer to withdraw a proposal from
// the asset collection and from his own collection. // the asset collection and from his own collection.
public async DeleteTransferAgreement(ctx: Context): Promise<void> { public async DeleteTransferAgreement(ctx: Context): Promise<void> {
// Value is private, therefore it gets passed in transient field // Value is private, therefore it gets passed in transient field
const transientMap = ctx.stub.getTransient(); const transientMap = ctx.stub.getTransient();
// Persist the JSON bytes as-is so that there is no risk of nondeterministic marshaling. const agreementDelete = new TransientAssetDelete(transientMap);
const valueJSONasBytes = transientMap.get('agreement_delete');
if (valueJSONasBytes.length === 0) {
throw new Error('agreement to delete not found in the transient map');
}
const jsonBytesToString = String.fromCharCode(...valueJSONasBytes);
const jsonFromString = JSON.parse(jsonBytesToString);
if (jsonFromString.assetID.length === 0) {
throw new Error('assetID field must be a non-empty string');
}
// Verify that the client is submitting request to peer in their organization // Verify that the client is submitting request to peer in their organization
const err = await this.verifyClientOrgMatchesPeerOrg(ctx); this.verifyClientOrgMatchesPeerOrg(ctx);
if (err !== null) {
throw new Error('DeleteTranferAgreement cannot be performed: Error ' + err);
}
// Delete private details of agreement // Delete private details of agreement
// Get proposers collection. // Get proposers collection.
const orgCollection = await this.getCollectionName(ctx); const orgCollection = this.getCollectionName(ctx);
// Delete the transfer agreement from the asset collection // Delete the transfer agreement from the asset collection
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [jsonFromString.assetID]); const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [agreementDelete.assetID]);
// get the transfer_agreement // get the transfer_agreement
const valAsbytes = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey); const valAsbytes = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey);
if (valAsbytes.length === 0) { if (valAsbytes.length === 0) {
throw new Error(`asset's transfer_agreement does not exist: ${jsonFromString.assetID}`); throw new Error(`asset's transfer_agreement does not exist: ${agreementDelete.assetID}`);
} }
// Delete the asset // Delete the asset
await ctx.stub.deletePrivateData(orgCollection, jsonFromString.assetID); await ctx.stub.deletePrivateData(orgCollection, agreementDelete.assetID);
// Delete transfer agreement record, remove agreement from state // Delete transfer agreement record, remove agreement from state
await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey); await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey);
return null;
} }
/* /*
GETTERS GETTERS
*/ */
// ReadAsset reads the information from collection // ReadAsset reads the information from collection
@Transaction() @Transaction()
public async ReadAsset(ctx: Context, id: string): Promise<Asset> { public async ReadAsset(ctx: Context, id: string): Promise<Asset> {
@ -303,33 +217,21 @@ export class AssetTransfer extends Contract {
if (assetAsBytes.length === 0) { if (assetAsBytes.length === 0) {
throw new Error(id + ' does not exist in collection ' + assetCollection); throw new Error(id + ' does not exist in collection ' + assetCollection);
} }
const jsonBytesToString = String.fromCharCode(...assetAsBytes); return Asset.fromBytes(assetAsBytes);
const jsonFromBytes = JSON.parse(jsonBytesToString);
const asset: Asset = {
ID: jsonFromBytes.ID,
Color: jsonFromBytes.Color,
Size: jsonFromBytes.Size,
Owner: jsonFromBytes.Owner,
};
return asset;
} }
// ReadAssetPrivateDetails reads the asset private details in organization specific collection // ReadAssetPrivateDetails reads the asset private details in organization specific collection
@Transaction() @Transaction()
public async ReadAssetPrivateDetails(ctx: Context, collection: string, id: string): Promise<AssetPrivateDetails> { public async ReadAssetPrivateDetails(ctx: Context, collection: string, id: string): Promise<AssetPrivateDetails> {
// Check if asset already exists // Check if asset already exists
const assetAsBytes = await ctx.stub.getPrivateData(collection, id); const detailBytes = await ctx.stub.getPrivateData(collection, id);
// No Asset found, return empty response // No Asset found, return empty response
if (assetAsBytes.length === 0) { if (detailBytes.length === 0) {
throw new Error(id + ' does not exist in collection ' + collection); throw new Error(id + ' does not exist in collection ' + collection);
} }
const jsonBytesToString = String.fromCharCode(...assetAsBytes); return AssetPrivateDetails.fromBytes(detailBytes);
const jsonFromBytes = JSON.parse(jsonBytesToString);
const asset: AssetPrivateDetails = {
ID: jsonFromBytes.ID,
AppraisedValue: jsonFromBytes.AppraisedValue,
};
return asset;
} }
// ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection // ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection
@Transaction() @Transaction()
public async ReadTransferAgreement(ctx: Context, assetID: string): Promise<TransferAgreement> { public async ReadTransferAgreement(ctx: Context, assetID: string): Promise<TransferAgreement> {
@ -341,11 +243,11 @@ export class AssetTransfer extends Contract {
if (buyerIdentity.length === 0) { if (buyerIdentity.length === 0) {
throw new Error(`TransferAgreement for ${assetID} does not exist `); throw new Error(`TransferAgreement for ${assetID} does not exist `);
} }
const agreement: TransferAgreement = {
return {
ID: assetID, ID: assetID,
BuyerID: String(buyerIdentity), BuyerID: String(buyerIdentity),
}; };
return agreement;
} }
// GetAssetByRange performs a range query based on the start and end keys provided. Range // GetAssetByRange performs a range query based on the start and end keys provided. Range
@ -356,14 +258,8 @@ export class AssetTransfer extends Contract {
const resultsIterator = ctx.stub.getPrivateDataByRange(assetCollection, startKey, endKey); const resultsIterator = ctx.stub.getPrivateDataByRange(assetCollection, startKey, endKey);
const results: Asset[] = []; const results: Asset[] = [];
for await (const res of resultsIterator) { for await (const res of resultsIterator) {
const resBytesToString = String.fromCharCode(...res.value); const asset = Asset.fromBytes(res.value);
const jsonFromString = JSON.parse(resBytesToString); results.push(asset);
results.push({
ID: jsonFromString.ID,
Color: jsonFromString.Color,
Size: jsonFromString.Size,
Owner: jsonFromString.Owner,
});
} }
return results; return results;
} }
@ -373,7 +269,7 @@ export class AssetTransfer extends Contract {
// verifyAgreement is an internal helper function used by TransferAsset to verify // verifyAgreement is an internal helper function used by TransferAsset to verify
// that the transfer is being initiated by the owner and that the buyer has agreed // that the transfer is being initiated by the owner and that the buyer has agreed
// to the same appraisal value as the owner // to the same appraisal value as the owner
public async verifyAgreement(ctx: Context, assetID: string, owner: string, buyerMSP: string): (Promise<void>) { public async verifyAgreement(ctx: Context, assetID: string, owner: string, buyerMSP: string): Promise<void> {
// Check 1: verify that the transfer is being initiatied by the owner // Check 1: verify that the transfer is being initiatied by the owner
// Get ID of submitting client identity // Get ID of submitting client identity
const clientID = ctx.clientIdentity.getID(); const clientID = ctx.clientIdentity.getID();
@ -382,7 +278,7 @@ export class AssetTransfer extends Contract {
} }
// Check 2: verify that the buyer has agreed to the appraised value // Check 2: verify that the buyer has agreed to the appraised value
// Get collection names // Get collection names
const collectionOwner = await this.getCollectionName(ctx); // get owner collection from caller identity const collectionOwner = this.getCollectionName(ctx); // get owner collection from caller identity
const collectionBuyer = buyerMSP + 'PrivateCollection'; // get buyers collection const collectionBuyer = buyerMSP + 'PrivateCollection'; // get buyers collection
// Get hash of owners agreed to value // Get hash of owners agreed to value
@ -394,16 +290,15 @@ export class AssetTransfer extends Contract {
// Get hash of buyers agreed to value // Get hash of buyers agreed to value
const buyerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionBuyer, assetID); const buyerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionBuyer, assetID);
if (buyerAppraisedValueHash.length === 0) { if (buyerAppraisedValueHash.length === 0) {
throw new Error(`hash of appraised value for ${assetID} does not exist in collection ${buyerAppraisedValueHash} . AgreeToTransfer must be called by the buyer first`); throw new Error(`hash of appraised value for ${assetID} does not exist in collection ${collectionBuyer}. AgreeToTransfer must be called by the buyer first`);
} }
// Verify that the two hashes match // Verify that the two hashes match
if (ownerAppraisedValueHash.toString() !== buyerAppraisedValueHash.toString()) { if (ownerAppraisedValueHash.toString() !== buyerAppraisedValueHash.toString()) {
throw new Error(`hash for appraised value for owner ${ownerAppraisedValueHash} does not value for seller ${buyerAppraisedValueHash}`); throw new Error(`hash for appraised value for owner ${Buffer.from(ownerAppraisedValueHash).toString('hex')} does not match value for seller ${Buffer.from(buyerAppraisedValueHash).toString('hex')}`);
} }
return null;
} }
// getCollectionName is an internal helper function to get collection of submitting client identity. // getCollectionName is an internal helper function to get collection of submitting client identity.
public async getCollectionName(ctx: Context): Promise<string> { public getCollectionName(ctx: Context): string {
// Get the MSP ID of submitting client identity // Get the MSP ID of submitting client identity
const clientMSPID = ctx.clientIdentity.getMSPID(); const clientMSPID = ctx.clientIdentity.getMSPID();
// Create the collection name // Create the collection name
@ -412,7 +307,7 @@ export class AssetTransfer extends Contract {
return orgCollection; return orgCollection;
} }
// Get ID of submitting client identity // Get ID of submitting client identity
public async submittingClientIdentity(ctx: Context): (Promise<string>) { public submittingClientIdentity(ctx: Context): string {
const b64ID = ctx.clientIdentity.getID(); const b64ID = ctx.clientIdentity.getID();
@ -422,7 +317,7 @@ export class AssetTransfer extends Contract {
return String(decodeID); return String(decodeID);
} }
// verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and matches peer org id. // verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and matches peer org id.
public async verifyClientOrgMatchesPeerOrg(ctx: Context): (Promise<void>) { public verifyClientOrgMatchesPeerOrg(ctx: Context): void {
const clientMSPID = ctx.clientIdentity.getMSPID(); const clientMSPID = ctx.clientIdentity.getMSPID();
@ -431,9 +326,6 @@ export class AssetTransfer extends Contract {
if (clientMSPID !== peerMSPID) { if (clientMSPID !== peerMSPID) {
throw new Error('client from org %v is not authorized to read or write private data from an org ' + clientMSPID + ' peer ' + peerMSPID); throw new Error('client from org %v is not authorized to read or write private data from an org ' + clientMSPID + ' peer ' + peerMSPID);
} }
return null;
} }
// =======Rich queries ========================================================================= // =======Rich queries =========================================================================
// Two examples of rich queries are provided below (parameterized query and ad hoc query). // Two examples of rich queries are provided below (parameterized query and ad hoc query).
@ -457,13 +349,15 @@ export class AssetTransfer extends Contract {
// ========================================================================================= // =========================================================================================
public async QueryAssetByOwner(ctx: Context, assetType: string, owner: string): Promise<Asset[]> { public async QueryAssetByOwner(ctx: Context, assetType: string, owner: string): Promise<Asset[]> {
const queryString = `{\'selector\':{\'objectType\':\'${assetType}\',\'owner\':\'${owner}\'}}`; const queryString = `{'selector':{'objectType':'${assetType}','owner':'${owner}'}}`;
return await this.getQueryResultForQueryString(ctx, queryString); return await this.getQueryResultForQueryString(ctx, queryString);
} }
public async QueryAssets(ctx: Context, queryString: string): Promise<Asset[]> {
return await this.getQueryResultForQueryString(ctx, queryString); public QueryAssets(ctx: Context, queryString: string): Promise<Asset[]> {
return this.getQueryResultForQueryString(ctx, queryString);
} }
public async getQueryResultForQueryString(ctx: Context, queryString: string): Promise<Asset[]> { public async getQueryResultForQueryString(ctx: Context, queryString: string): Promise<Asset[]> {
const resultsIterator = ctx.stub.getPrivateDataQueryResult(assetCollection, queryString); const resultsIterator = ctx.stub.getPrivateDataQueryResult(assetCollection, queryString);
@ -471,14 +365,8 @@ export class AssetTransfer extends Contract {
const results: Asset[] = []; const results: Asset[] = [];
for await (const res of resultsIterator) { for await (const res of resultsIterator) {
const resBytesToString = String.fromCharCode(...res.value); const asset = Asset.fromBytes(res.value);
const jsonFromString = JSON.parse(resBytesToString); results.push(asset);
results.push({
ID: jsonFromString.ID,
Color: jsonFromString.Color,
Size: jsonFromString.Size,
Owner: jsonFromString.Owner,
});
} }
return results; return results;

View file

@ -2,13 +2,31 @@
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
*/ */
import { Object, Property } from 'fabric-contract-api'; import { Object, Property } from "fabric-contract-api";
import { nonEmptyString, positiveNumber } from "./utils";
@Object() @Object()
// AssetPrivateDetails describes details that are private to owners // AssetPrivateDetails describes details that are private to owners
export class AssetPrivateDetails { export class AssetPrivateDetails {
@Property() @Property()
public ID: string; ID: string = "";
@Property() @Property()
public AppraisedValue: number; AppraisedValue: number = 0;
static fromBytes(bytes: Uint8Array): AssetPrivateDetails {
if (bytes.length === 0) {
throw new Error("no asset private details");
}
const json = Buffer.from(bytes).toString();
const properties = JSON.parse(json) as Partial<AssetPrivateDetails>;
const result = new AssetPrivateDetails();
result.ID = nonEmptyString(properties.ID, "ID field must be a non-empty string");
result.AppraisedValue = positiveNumber(
properties.AppraisedValue,
"AppraisedValue field must be a positive integer"
);
return result;
}
} }

View file

@ -2,12 +2,114 @@
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
*/ */
import { Object, Property } from 'fabric-contract-api'; import { nonEmptyString, positiveNumber } from "./utils";
@Object() export class TransientAssetProperties {
export class AssetTransferTransientInput { objectType: string;
@Property() assetID: string;
public ID: string; color: string;
@Property() size: number;
public BuyerMSP: string; appraisedValue: number;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("asset_properties");
if (!transient?.length) {
throw new Error("no asset properties");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetProperties>;
this.objectType = nonEmptyString(properties.objectType, "objectType field must be a non-empty string");
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
this.color = nonEmptyString(properties.color, "color field must be a non-empty string");
this.size = positiveNumber(properties.size, "size field must be a positive integer");
this.appraisedValue = positiveNumber(
properties.appraisedValue,
"appraisedValue field must be a positive integer"
);
}
}
export class TransientAssetValue {
assetID: string;
appraisedValue: number;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("asset_value");
if (!transient?.length) {
throw new Error("no asset value");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetValue>;
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
this.appraisedValue = positiveNumber(
properties.appraisedValue,
"appraisedValue field must be a positive integer"
);
}
}
export class TransientAssetOwner {
assetID: string;
buyerMSP: string;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("asset_owner");
if (!transient?.length) {
throw new Error("no asset owner");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
this.buyerMSP = nonEmptyString(properties.buyerMSP, "buyerMSP field must be a non-empty string");
}
}
export class TransientAssetDelete {
assetID: string;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("asset_delete");
if (!transient?.length) {
throw new Error("no asset delete");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
}
}
export class TransientAssetPurge {
assetID: string;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("asset_purge");
if (!transient?.length) {
throw new Error("no asset purge");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
}
}
export class TransientAgreementDelete {
assetID: string;
constructor(transientMap: Map<string, Uint8Array>) {
const transient = transientMap.get("agreement_delete");
if (!transient?.length) {
throw new Error("no agreement delete");
}
const json = Buffer.from(transient).toString();
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
}
} }

View file

@ -2,8 +2,9 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { type Contract } from 'fabric-contract-api';
import {AssetTransfer} from './assetTransfer'; import {AssetTransfer} from './assetTransfer';
export {AssetTransfer} from './assetTransfer'; export {AssetTransfer} from './assetTransfer';
export const contracts: any[] = [AssetTransfer]; export const contracts: typeof Contract[] = [AssetTransfer];

View file

@ -2,14 +2,13 @@
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
*/ */
import { Object, Property } from 'fabric-contract-api'; import { Object, Property } from "fabric-contract-api";
@Object() @Object()
// TransferAgreement describes the buyer agreement returned by ReadTransferAgreement // TransferAgreement describes the buyer agreement returned by ReadTransferAgreement
export class TransferAgreement { export class TransferAgreement {
@Property() @Property()
public ID: string; ID: string = "";
@Property() @Property()
public BuyerID: string; BuyerID: string = "";
} }

View file

@ -0,0 +1,19 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
export function nonEmptyString(arg: unknown, errorMessage: string): string {
if (typeof arg !== "string" || arg.length === 0) {
throw new Error(errorMessage);
}
return arg;
}
export function positiveNumber(arg: unknown, errorMessage: string): number {
if (typeof arg !== "number" || arg < 1) {
throw new Error(errorMessage);
}
return arg;
}

View file

@ -1,19 +1,17 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"outDir": "dist",
"target": "es2017",
"moduleResolution": "node",
"module": "commonjs",
"esModuleInterop": true,
"declaration": true, "declaration": true,
"sourceMap": true "declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
}, },
"include": [ "include": ["src/"]
"./src/**/*"
],
"exclude": [
"./src/**/*.spec.ts"
]
} }

View file

@ -1,23 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"indent": [true, "spaces", 4],
"linebreak-style": [true, "LF"],
"quotemark": [true, "single"],
"semicolon": [true, "always"],
"no-console": false,
"curly": true,
"triple-equals": true,
"no-string-throw": true,
"no-var-keyword": true,
"no-trailing-whitespace": true,
"object-literal-key-quotes": [true, "as-needed"],
"object-literal-sort-keys": false,
"max-line-length": false
},
"rulesDirectory": []
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@
"node": ">=18" "node": ">=18"
}, },
"scripts": { "scripts": {
"lint": "eslint ./src --ext .ts", "lint": "eslint src",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "echo 'No tests implemented'", "test": "echo 'No tests implemented'",
"start": "fabric-chaincode-node start", "start": "fabric-chaincode-node start",
@ -21,15 +21,15 @@
"author": "Hyperledger", "author": "Hyperledger",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fabric-contract-api": "~2.5.4", "fabric-contract-api": "~2.5.5",
"fabric-shim": "~2.5.4" "fabric-shim": "~2.5.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node18": "^18.2.2", "@types/node": "^18.19.33",
"@types/node": "^18.17.17", "@eslint/js": "^9.3.0",
"@typescript-eslint/eslint-plugin": "^6.7.0", "@tsconfig/node18": "^18.2.4",
"@typescript-eslint/parser": "^6.7.0", "eslint": "^8.57.0",
"eslint": "^8.49.0", "typescript": "~5.4.5",
"typescript": "~5.2.0" "typescript-eslint": "^7.11.0"
} }
} }

View file

@ -90,7 +90,7 @@ export class AssetContract extends Contract {
// AssetExists returns true when asset with given ID exists // AssetExists returns true when asset with given ID exists
public async AssetExists(ctx: Context, assetId: string): Promise<boolean> { public async AssetExists(ctx: Context, assetId: string): Promise<boolean> {
const buffer = await ctx.stub.getState(assetId); const buffer = await ctx.stub.getState(assetId);
return (!!buffer && buffer.length > 0); return buffer.length > 0;
} }
// getClientOrgId gets the client's OrgId (MSPID) // getClientOrgId gets the client's OrgId (MSPID)

View file

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