Copy asset-transfer-secured-agreement to adapt to Varion requirements

This commit is contained in:
Ferry Ariawan 2024-06-06 18:39:18 +07:00
parent 33f176917a
commit be5d9fd6e2
19 changed files with 3540 additions and 0 deletions

65
varion-contract/README.md Normal file
View file

@ -0,0 +1,65 @@
# Asset transfer secured agreement sample
The asset transfer events sample demonstrates how to transfer a private asset between two organizations without publicly sharing data .
## About the sample
This sample includes smart contract and application code in multiple languages. This sample shows how Fabric features state based endorsement, private data, and access control to provide secured transactions.
### Application
Refer [Secured asset transfer in Fabric](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) for application details .
### Smart Contract
The smart contract (in folder `chaincode-go`) implements the following functions to support the application:
- CreateAsset
- ChangePublicDescription
- AgreeToSell
- AgreeToBuy
- VerifyAssetProperties
- TransferAsset
- ReadAsset
- GetAssetPrivateProperties
- GetAssetSalesPrice
- GetAssetBidPrice
- GetAssetHashId
- QueryAssetSaleAgreements
- QueryAssetBuyAgreements
- QueryAssetHistory
## Running the sample
Like other samples, the Fabric test network is used to deploy and run this sample. Follow these steps in order:
1. Create the test network and a channel (from the `test-network` folder).
```
./network.sh up createChannel -c mychannel -ca
```
1. Deploy the smart contract implementations.
```
# To deploy the go chaincode implementation
./network.sh deployCC -ccn secured -ccp ../asset-transfer-secured-agreement/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')"
```
1. Run the application (from the `asset-transfer-secured-agreement` folder).
```
# To run the Typescript sample application
cd application-gateway-typescript
npm install
npm start
# To run the Javascript sample application
cd application-javascript
node app.js
```
## Clean up
When you are finished, you can bring down the test network (from the `test-network` folder). The command will remove all the nodes of the test network, and delete any ledger data that you created.
```
./network.sh down
```

View file

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

View file

@ -0,0 +1,14 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Coverage directory used by tools like istanbul
coverage
# Dependency directories
node_modules/
jspm_packages/
# Compiled TypeScript files
dist

View file

@ -0,0 +1,33 @@
{
"name": "asset-transfer-basic",
"version": "1.0.0",
"description": "Asset Transfer Secured Agreement Application implemented in typeScript using fabric-gateway",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsc",
"build:watch": "tsc -w",
"lint": "eslint . --ext .ts",
"prepare": "npm run build",
"pretest": "npm run lint",
"start": "node dist/app.js"
},
"engineStrict": true,
"author": "Hyperledger",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.9.7",
"@hyperledger/fabric-gateway": "~1.4.0"
},
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.6",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"typescript": "~5.2.2"
}
}

View file

@ -0,0 +1,215 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import { connect } from '@hyperledger/fabric-gateway';
import { newGrpcConnection, newIdentity, newSigner, tlsCertPathOrg1, peerEndpointOrg1, peerNameOrg1, certDirectoryPathOrg1, mspIdOrg1, keyDirectoryPathOrg1, tlsCertPathOrg2, peerEndpointOrg2, peerNameOrg2, certDirectoryPathOrg2, mspIdOrg2, keyDirectoryPathOrg2 } from './connect';
import { ContractWrapper } from './contractWrapper';
import { RED, RESET } from './utils';
const channelName = 'mychannel';
const chaincodeName = 'secured';
// Use a random key so that we can run multiple times
const now = Date.now().toString();
let assetKey: string;
async function main(): Promise<void> {
// The gRPC client connection from org1 should be shared by all Gateway connections to this endpoint.
const clientOrg1 = await newGrpcConnection(
tlsCertPathOrg1,
peerEndpointOrg1,
peerNameOrg1
);
const gatewayOrg1 = connect({
client: clientOrg1,
identity: await newIdentity(certDirectoryPathOrg1, mspIdOrg1),
signer: await newSigner(keyDirectoryPathOrg1),
});
// The gRPC client connection from org2 should be shared by all Gateway connections to this endpoint.
const clientOrg2 = await newGrpcConnection(
tlsCertPathOrg2,
peerEndpointOrg2,
peerNameOrg2
);
const gatewayOrg2 = connect({
client: clientOrg2,
identity: await newIdentity(certDirectoryPathOrg2, mspIdOrg2),
signer: await newSigner(keyDirectoryPathOrg2),
});
try {
// Get the smart contract from the network for Org1.
const contractOrg1 = gatewayOrg1.getNetwork(channelName).getContract(chaincodeName);
const contractWrapperOrg1 = new ContractWrapper(contractOrg1, mspIdOrg1);
// Get the smart contract from the network for Org2.
const contractOrg2 = gatewayOrg2.getNetwork(channelName).getContract(chaincodeName);
const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2);
// Create an asset by organization Org1, this only requires the owning organization to endorse.
assetKey = await contractWrapperOrg1.createAsset(mspIdOrg1,
`Asset owned by ${mspIdOrg1} is not for sale`, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
// Org1 should be able to read the private data details of the asset.
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
// Org2 is not the owner and does not have the private details, read expected to fail.
try {
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`);
}
// Org1 updates the assets public description.
await contractWrapperOrg1.changePublicDescription({assetId: assetKey,
ownerOrg: mspIdOrg1,
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is for sale`});
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
// This is an update to the public state and requires the owner(Org1) to endorse and sent by the owner org client (Org1).
// Since the client is from Org2, which is not the owner, this will fail.
try{
await contractWrapperOrg2.changePublicDescription({assetId: assetKey,
ownerOrg: mspIdOrg1,
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${e}${RESET}`);
}
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
// Agree to a sell by org1.
await contractWrapperOrg1.agreeToSell({
assetId: assetKey,
price: 110,
tradeId: now,
});
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details,
// so the hash of the details may be checked by the chaincode.
await contractWrapperOrg2.verifyAssetProperties(assetKey, {color:'blue', size:35});
// Agree to a buy by org2.
await contractWrapperOrg2.agreeToBuy( {assetId: assetKey,
price: 100,
tradeId: now}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Org1 should be able to read the sale price of this asset.
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
// Org2 has not set a sale price and this should fail.
try{
await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${e}${RESET}`);
}
// Org1 has not agreed to buy so this should fail.
try{
await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${e}${RESET}`);
}
// Org2 should be able to see the price it has agreed.
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
// Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price are not the same.
try{
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`);
}
// 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});
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
// Org1 should be able to read the private data details of the asset.
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
// Org1 should be able to read the sale price of this asset.
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
// Org2 should be able to see the price it has agreed.
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
// Org2 user will try to transfer the asset to Org1.
// This will fail as the owner is Org1.
try{
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${e}${RESET}`);
}
// Org1 will transfer the asset to Org2.
// This will now complete as the sell price and the bid price are the same.
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
// Org2 should be able to read the private data details of this asset.
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg2);
// Org1 should not be able to read the private data details of this asset, expected to fail.
try{
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2);
} catch(e) {
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${e}${RESET}`);
}
// This is an update to the public state and requires only the owner to endorse.
// Org2 wants to indicate that the items is no longer for sale.
await contractWrapperOrg2.changePublicDescription( {assetId: assetKey, ownerOrg: mspIdOrg2, publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
// Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
// Read the public details by org2.
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
} finally {
gatewayOrg1.close();
gatewayOrg2.close();
clientOrg1.close();
clientOrg2.close();
}
}
main().catch(error => {
console.error('******** FAILED to run the application:', error);
process.exitCode = 1;
});

View file

@ -0,0 +1,107 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import * as grpc from '@grpc/grpc-js';
import { Identity, Signer, signers } from '@hyperledger/fabric-gateway';
import * as crypto from 'crypto';
import { promises as fs } from 'fs';
import * as path from 'path';
// MSP Id's of Organizations
export const mspIdOrg1 = 'Org1MSP';
export const mspIdOrg2 = 'Org2MSP';
// Path to org1 crypto materials.
export const cryptoPathOrg1 = path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com');
// Path to user private key directory.
export const keyDirectoryPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'keystore');
// Path to user certificate.
export const certDirectoryPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'signcerts');
// Path to peer tls certificate.
export const tlsCertPathOrg1 = path.resolve(cryptoPathOrg1, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt');
// Path to org2 crypto materials.
export const cryptoPathOrg2 = path.resolve(
__dirname,
'..',
'..',
'..',
'test-network',
'organizations',
'peerOrganizations',
'org2.example.com'
);
// Path to org2 user private key directory.
export const keyDirectoryPathOrg2 = path.resolve(
cryptoPathOrg2,
'users',
'User1@org2.example.com',
'msp',
'keystore'
);
// Path to org2 user certificate.
export const certDirectoryPathOrg2 = path.resolve(
cryptoPathOrg2,
'users',
'User1@org2.example.com',
'msp',
'signcerts'
);
// Path to org2 peer tls certificate.
export const tlsCertPathOrg2 = path.resolve(
cryptoPathOrg2,
'peers',
'peer0.org2.example.com',
'tls',
'ca.crt'
);
// Gateway peer endpoint.
export const peerEndpointOrg1 = 'localhost:7051';
export const peerEndpointOrg2 = 'localhost:9051';
// Gateway peer container name.
export const peerNameOrg1 = 'peer0.org1.example.com';
export const peerNameOrg2 = 'peer0.org2.example.com';
// Collection Names
export const org1PrivateCollectionName = 'Org1MSPPrivateCollection';
export const org2PrivateCollectionName = 'Org2MSPPrivateCollection';
export async function newGrpcConnection(
tlsCertPath: string,
peerEndpoint: string,
peerName: string
): Promise<grpc.Client> {
const tlsRootCert = await fs.readFile(tlsCertPath);
const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
return new grpc.Client(peerEndpoint, tlsCredentials, {
'grpc.ssl_target_name_override': peerName,
});
}
export async function newIdentity(certDirectoryPath: string, mspId: string): Promise<Identity> {
const certPath = await getFirstDirFileName(certDirectoryPath);
const credentials = await fs.readFile(certPath);
return { mspId, credentials };
}
export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
const keyPath = await getFirstDirFileName(keyDirectoryPath);
const privateKeyPem = await fs.readFile(keyPath);
const privateKey = crypto.createPrivateKey(privateKeyPem);
return signers.newPrivateKeySigner(privateKey);
}
async function getFirstDirFileName(dirPath: string): Promise<string> {
const files = await fs.readdir(dirPath);
return path.join(dirPath, files[0]);
}

View file

@ -0,0 +1,276 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Contract } from '@hyperledger/fabric-gateway';
import { TextDecoder } from 'util';
import { GREEN, parse, RED, RESET } from './utils';
import crpto from 'crypto';
import { mspIdOrg2 } from './connect';
const randomBytes = crpto.randomBytes(256).toString('hex');
interface AssetJSON {
objectType: string;
assetID: string;
ownerOrg: string;
publicDescription: string;
}
interface AssetPropertiesJSON {
objectType: string;
color: string;
size: number;
salt: string;
}
interface AssetPriceJSON {
assetID: string;
price: number;
tradeID: string;
}
export interface AssetPrivateData {
ObjectType: string;
Color: string;
Size: number;
}
export interface Asset {
assetId: string;
ownerOrg: string;
publicDescription: string;
}
export interface AssetProperties {
color: string;
size: number;
}
export interface AssetPrice {
assetId: string;
price: number;
tradeId: string;
}
export class ContractWrapper {
readonly #contract: Contract;
readonly #org: string;
readonly #utf8Decoder = new TextDecoder();
readonly #randomBytes: string = randomBytes;
#endorsingOrgs: { [id: string]: string[] };
public constructor(contract: Contract, org: string) {
this.#contract = contract;
this.#org = org;
this.#endorsingOrgs = {};
}
public async createAsset(ownerOrg: string, publicDescription: string, privateData: AssetPrivateData): Promise<string> {
console.log(`${GREEN}--> Submit Transaction: CreateAsset as ${ownerOrg} - endorsed by Org1.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {
objectType: 'asset_properties',
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
const resultBytes = await this.#contract.submit('CreateAsset', {
arguments: [publicDescription],
transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)},
});
const assetID = this.#utf8Decoder.decode(resultBytes);
this.#endorsingOrgs[assetID] = [ownerOrg];
console.log(`*** Result: committed, asset ${assetID} is owned by ${ownerOrg}`);
return assetID;
}
public async readAsset(assetKey: string, ownerOrg: string): Promise<void> {
console.log(`${GREEN}--> Evaluate Transactions: ReadAsset as ${this.#org}, - ${assetKey} should be owned by ${ownerOrg}.${RESET}`);
const resultBytes = await this.#contract.evaluateTransaction('ReadAsset', assetKey);
const result = this.#utf8Decoder.decode(resultBytes);
if (result.length !== 0) {
const json = parse<AssetJSON>(result);
if (json.ownerOrg === ownerOrg) {
console.log(`*** Result from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC: ${json.publicDescription}`);
} else {
console.log(`${RED}*** Failed owner check from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC:${json.publicDescription}.${RESET}`);
}
} else {
throw new Error('No Asset Found');
}
}
public async getAssetPrivateProperties(assetKey: string, ownerOrg: string): Promise<void> {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetPrivateProperties, - ${assetKey} from organization ${this.#org}.${RESET}`);
if(this.#org !== ownerOrg) {
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner and does not have the private details.${RESET}`);
}
const resultBytes = await this.#contract.evaluateTransaction('GetAssetPrivateProperties', assetKey);
const resultString = this.#utf8Decoder.decode(resultBytes);
const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = {
color: json.color,
size: json.size,
};
console.log('*** Result:', result);
}
public async changePublicDescription(asset: Asset): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${asset.assetId}, as ${this.#org} - endorse by ${this.#org}.${RESET}`);
if (asset.ownerOrg !== this.#org) {
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner.${RESET}`);
}
await this.#contract.submit('ChangePublicDescription', {
arguments:[asset.assetId, asset.publicDescription],
endorsingOrganizations: this.#endorsingOrgs[asset.assetId]
});
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
}
public async agreeToSell(assetPrice: AssetPrice): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = {
assetID:assetPrice.assetId,
price:assetPrice.price,
tradeID:assetPrice.tradeId
};
await this.#contract.submit('AgreeToSell', {
arguments:[assetPrice.assetId],
transientData: {asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`);
}
public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
color: assetProperties.color,
size: assetProperties.size,
salt: this.#randomBytes };
const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', {
arguments:[assetId],
transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)},
});
const resultString = this.#utf8Decoder.decode(resultBytes);
if (resultString.length !== 0) {
const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = {
color: json.color,
size: json.size
};
if (result) {
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
} else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
}
} else {
throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
}
}
public async agreeToBuy(assetPrice: AssetPrice, privateData: AssetPrivateData): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {
objectType: 'asset_properties',
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
const assetPriceJSON: AssetPriceJSON = {
assetID: assetPrice.assetId,
price: assetPrice.price,
tradeID: assetPrice.tradeId
};
await this.#contract.submit('AgreeToBuy', {
arguments:[assetPrice.assetId],
transientData: {
asset_price: JSON.stringify(assetPriceJSON),
asset_properties: JSON.stringify(assetPropertiesJSON)
},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
});
console.log(`*** Result: committed, ${this.#org} has agreed to buy asset ${assetPrice.assetId} for 100`);
}
public async getAssetSalesPrice(assetKey: string, ownerOrg: string): Promise<void> {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetSalesPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
if(this.#org !== ownerOrg) {
console.log(`${GREEN}* Expected to fail as ${this.#org} has not set a sale price.${RESET}`);
}
const resultBytes = await this.#contract.evaluateTransaction('GetAssetSalesPrice', assetKey);
const resultString = this.#utf8Decoder.decode(resultBytes);
const json = parse<AssetPriceJSON>(resultString);
const result: AssetPrice = {
assetId: json.assetID,
price: json.price,
tradeId: json.tradeID
};
console.log('*** Result: GetAssetSalesPrice', result);
}
public async getAssetBidPrice(assetKey: string, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetBidPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
if(this.#org !== buyerOrgID){
console.log(`${GREEN}* Expected to fail as ${this.#org} has not agreed to buy.${RESET}`);
}
const resultBytes = await this.#contract.evaluateTransaction('GetAssetBidPrice', assetKey);
const resultString = this.#utf8Decoder.decode(resultBytes);
const json = parse<AssetPriceJSON>(resultString);
const result: AssetPrice = {
assetId: json.assetID,
price: json.price,
tradeId: json.tradeID,
};
console.log('*** Result: GetAssetBidPrice', result);
}
public async transferAsset(assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`);
if (this.#org !== ownerOrgID) {
console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`);
} else if (assetPrice.price === 110) {
console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`);
}
const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId};
await this.#contract.submit('TransferAsset', {
arguments:[assetPrice.assetId, buyerOrgID],
transientData: { asset_price: JSON.stringify(assetPriceJSON) },
endorsingOrganizations: endorsingOrganizations
});
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
export const RED = '\x1b[31m\n';
export const GREEN = '\x1b[32m\n';
export const RESET = '\x1b[0m';
export function parse<T>(data: string): T {
return JSON.parse(data);
}

View file

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

View file

@ -0,0 +1,5 @@
#
# SPDX-License-Identifier: Apache-2.0
#
coverage

View file

@ -0,0 +1,37 @@
/*
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
module.exports = {
env: {
node: true,
mocha: true
},
parserOptions: {
ecmaVersion: 8,
sourceType: 'script'
},
extends: 'eslint:recommended',
rules: {
indent: ['error', 'tab'],
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'no-unused-vars': ['error', { args: 'none' }],
'no-console': 'off',
curly: 'error',
eqeqeq: 'error',
'no-throw-literal': 'error',
strict: 'error',
'no-var': 'error',
'dot-notation': 'error',
'no-trailing-spaces': 'error',
'no-use-before-define': 'error',
'no-useless-call': 'error',
'no-with': 'error',
'operator-linebreak': 'error',
yoda: 'error',
'quote-props': ['error', 'as-needed']
}
};

View file

@ -0,0 +1,14 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Coverage directory used by tools like istanbul
coverage
# Dependency directories
node_modules/
jspm_packages/
package-lock.json
wallet
!wallet/.gitkeep

View file

@ -0,0 +1,544 @@
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
'use strict';
/**
* Application that uses implicit private data collections, state-based endorsement,
* and organization-based ownership and access control to keep data private and securely
* transfer an asset with the consent of both the current owner and buyer
* -- How to submit a transaction
* -- How to query
* -- How to limit the organizations involved in a transaction
*
* To see the SDK workings, try setting the logging to show on the console before running
* export HFC_LOGGING='{"debug":"console"}'
*/
// pre-requisites:
// - fabric-sample two organization test-network setup with two peers, ordering service,
// and 2 certificate authorities
// ===> from directory /fabric-samples/test-network
// ./network.sh up createChannel -ca
// - Use the asset-transfer-secured-agreement/chaincode-go chaincode deployed on
// the channel "mychannel". The following deploy command will package, install,
// approve, and commit the golang chaincode, all the actions it takes
// to deploy a chaincode to a channel with the endorsement and private collection
// settings.
// ===> from directory /fabric-samples/test-network
// ./network.sh deployCC -ccn secured -ccp ../asset-transfer-secured-agreement/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')"
//
// - Be sure that node.js is installed
// ===> from directory /fabric-samples/asset-transfer-secured-agreement/application-javascript
// node -v
// - npm installed code dependencies
// ===> from directory /fabric-samples/asset-transfer-secured-agreement/application-javascript
// npm install
// - to run this test application
// ===> from directory /fabric-samples/asset-transfer-secured-agreement/application-javascript
// node app.js
// NOTE: If you see an error like these:
/*
Error in setup: Error: DiscoveryService: mychannel error: access denied
OR
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
*/
// Delete the /fabric-samples/asset-transfer-secured-agreement/application-javascript/wallet directory
// and retry this application.
//
// The certificate authority must have been restarted and the saved certificates for the
// admin and application user are not valid. Deleting the wallet store will force these to be reset
// with the new certificate authority.
//
const { Gateway, Wallets } = require('fabric-network');
const FabricCAServices = require('fabric-ca-client');
const path = require('path');
const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js');
const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js');
const channelName = 'mychannel';
const chaincodeName = 'secured';
const org1 = 'Org1MSP';
const org2 = 'Org2MSP';
const Org1UserId = 'appUser1';
const Org2UserId = 'appUser2';
const RED = '\x1b[31m\n';
const GREEN = '\x1b[32m\n';
const RESET = '\x1b[0m';
async function initGatewayForOrg1() {
console.log(`${GREEN}--> Fabric client user & Gateway init: Using Org1 identity to Org1 Peer${RESET}`);
// build an in memory object with the network configuration (also known as a connection profile)
const ccpOrg1 = buildCCPOrg1();
// build an instance of the fabric ca services client based on
// the information in the network configuration
const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com');
// setup the wallet to cache the credentials of the application user, on the app server locally
const walletPathOrg1 = path.join(__dirname, 'wallet', 'org1');
const walletOrg1 = await buildWallet(Wallets, walletPathOrg1);
// in a real application this would be done on an administrative flow, and only once
// stores admin identity in local wallet, if needed
await enrollAdmin(caOrg1Client, walletOrg1, org1);
// register & enroll application user with CA, which is used as client identify to make chaincode calls
// and stores app user identity in local wallet
// In a real application this would be done only when a new user was required to be added
// and would be part of an administrative flow
await registerAndEnrollUser(caOrg1Client, walletOrg1, org1, Org1UserId, 'org1.department1');
try {
// Create a new gateway for connecting to Org's peer node.
const gatewayOrg1 = new Gateway();
//connect using Discovery enabled
await gatewayOrg1.connect(ccpOrg1,
{ wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } });
return gatewayOrg1;
} catch (error) {
console.error(`Error in connecting to gateway for Org1: ${error}`);
process.exit(1);
}
}
async function initGatewayForOrg2() {
console.log(`${GREEN}--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer${RESET}`);
const ccpOrg2 = buildCCPOrg2();
const caOrg2Client = buildCAClient(FabricCAServices, ccpOrg2, 'ca.org2.example.com');
const walletPathOrg2 = path.join(__dirname, 'wallet', 'org2');
const walletOrg2 = await buildWallet(Wallets, walletPathOrg2);
await enrollAdmin(caOrg2Client, walletOrg2, org2);
await registerAndEnrollUser(caOrg2Client, walletOrg2, org2, Org2UserId, 'org2.department1');
try {
// Create a new gateway for connecting to Org's peer node.
const gatewayOrg2 = new Gateway();
await gatewayOrg2.connect(ccpOrg2,
{ wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } });
return gatewayOrg2;
} catch (error) {
console.error(`Error in connecting to gateway for Org2: ${error}`);
process.exit(1);
}
}
async function readPrivateAsset(assetKey, org, contract) {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetPrivateProperties, - ${assetKey} from organization ${org}${RESET}`);
try {
const resultBuffer = await contract.evaluateTransaction('GetAssetPrivateProperties', assetKey);
const asset = JSON.parse(resultBuffer.toString('utf8'));
console.log(`*** Result: GetAssetPrivateProperties, ${JSON.stringify(asset)}`);
} catch (evalError) {
console.log(`*** Failed evaluateTransaction readPrivateAsset: ${evalError}`);
}
}
async function readBidPrice(assetKey, org, contract) {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetBidPrice, - ${assetKey} from organization ${org}${RESET}`);
try {
const resultBuffer = await contract.evaluateTransaction('GetAssetBidPrice', assetKey);
const asset = JSON.parse(resultBuffer.toString('utf8'));
console.log(`*** Result: GetAssetBidPrice, ${JSON.stringify(asset)}`);
} catch (evalError) {
console.log(`*** Failed evaluateTransaction GetAssetBidPrice: ${evalError}`);
}
}
async function readSalePrice(assetKey, org, contract) {
console.log(`${GREEN}--> Evaluate Transaction: GetAssetSalesPrice, - ${assetKey} from organization ${org}${RESET}`);
try {
const resultBuffer = await contract.evaluateTransaction('GetAssetSalesPrice', assetKey);
const asset = JSON.parse(resultBuffer.toString('utf8'));
console.log(`*** Result: GetAssetSalesPrice, ${JSON.stringify(asset)}`);
} catch (evalError) {
console.log(`*** Failed evaluateTransaction GetAssetSalesPrice: ${evalError}`);
}
}
function checkAsset(org, resultBuffer, ownerOrg) {
let asset;
if (resultBuffer) {
asset = JSON.parse(resultBuffer.toString('utf8'));
}
if (asset) {
if (asset.ownerOrg === ownerOrg) {
console.log(`*** Result from ${org} - asset ${asset.assetID} owned by ${asset.ownerOrg} DESC:${asset.publicDescription}`);
} else {
console.log(`${RED}*** Failed owner check from ${org} - asset ${asset.assetID} owned by ${asset.ownerOrg} DESC:${asset.publicDescription}${RESET}`);
}
}
}
// This is not a real function for an application, this simulates when two applications are running
// from different organizations and what they would see if they were to both query the asset
async function readAssetByBothOrgs(assetKey, ownerOrg, contractOrg1, contractOrg2) {
console.log(`${GREEN}--> Evaluate Transactions: ReadAsset, - ${assetKey} should be owned by ${ownerOrg}${RESET}`);
let resultBuffer;
resultBuffer = await contractOrg1.evaluateTransaction('ReadAsset', assetKey);
checkAsset('Org1', resultBuffer, ownerOrg);
resultBuffer = await contractOrg2.evaluateTransaction('ReadAsset', assetKey);
checkAsset('Org2', resultBuffer, ownerOrg);
}
// This application uses fabric-samples/test-network based setup and the companion chaincode
// For this illustration, both Org1 & Org2 client identities will be used, however
// notice they are used by two different "gateway"s to simulate two different running
// applications from two different organizations.
async function main() {
console.log(`${GREEN} **** START ****${RESET}`);
try {
const randomNumber = Math.floor(Math.random() * 100) + 1;
let assetKey;
/** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */
const gatewayOrg1 = await initGatewayForOrg1();
const networkOrg1 = await gatewayOrg1.getNetwork(channelName);
const contractOrg1 = networkOrg1.getContract(chaincodeName);
/** ******* Fabric client init: Using Org2 identity to Org2 Peer ******* */
const gatewayOrg2 = await initGatewayForOrg2();
const networkOrg2 = await gatewayOrg2.getNetwork(channelName);
const contractOrg2 = networkOrg2.getContract(chaincodeName);
try {
let transaction;
try {
// Create an asset by organization Org1, this only requires the owning
// organization to endorse.
// With the gateway using discovery, we should limit the organizations used
// to endorse. This only requires knowledge of the Organizations and not
// the actual peers that may be active at any given time.
const asset_properties = {
object_type: 'asset_properties',
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: CreateAsset as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('CreateAsset');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string)
});
assetKey = await transaction.submit( `Asset owned by ${org1} is not for sale`);
console.log(`*** Result: committed, asset ${assetKey} is owned by Org1`);
} catch (createError) {
console.log(`${RED}*** Failed: CreateAsset - ${createError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2);
// Org1 should be able to read the private data details of this asset
await readPrivateAsset(assetKey, org1, contractOrg1);
// Org2 is not the owner and does not have the private details, this should fail
await readPrivateAsset(assetKey, org2, contractOrg2);
try {
// This is an update to the public state and requires only the owner to endorse.
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org1 - endorse by Org1${RESET}`);
transaction = contractOrg1.createTransaction('ChangePublicDescription');
transaction.setEndorsingOrganizations(org1);
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org1} is for sale`);
console.log(`*** Result: committed, asset ${assetKey} is now for sale by Org1`);
} catch (updateError) {
console.log(`${RED}*** Failed: ChangePublicDescription - ${updateError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2);
try {
// This is an update to the public state and requires the owner(Org1) to endorse and
// sent by the owner org client (Org1).
// Since the client is from Org2, which is not the owner, this will fail
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org2${RESET}`);
transaction = contractOrg2.createTransaction('ChangePublicDescription');
transaction.setEndorsingOrganizations(org2);
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`);
console.log(`${RESET}*** Failed: Org2 is not the owner and this should have failed${RESET}`);
} catch (updateError) {
console.log(`*** Success: ChangePublicDescription has failed endorsememnt by Org2 sent by Org2 - ${updateError}`);
}
try {
// This is an update to the public state and requires the owner(Org1) to endorse and
// sent by the owner org client (Org1).
// Since this is being sent by Org2, which is not the owner, this will fail
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org1${RESET}`);
transaction = contractOrg2.createTransaction('ChangePublicDescription');
transaction.setEndorsingOrganizations(org1);
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`);
console.log(`${RESET}*** Failed: Org2 is not the owner and this should have failed${RESET}`);
} catch (updateError) {
console.log(`*** Success: ChangePublicDescription has failed endorsement by Org1 sent by Org2 - ${updateError}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2);
try {
// Agree to a sell by Org1
const asset_price = {
asset_id: assetKey.toString(),
price: 110,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('AgreeToSell');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
//call agree to sell with desired price
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
}
try {
// check the private information about the asset from Org2
// Org1 would have to send Org2 these details, so the hash of the
// details may be checked by the chaincode.
const asset_properties = {
object_type: 'asset_properties',
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetKey} as Org2 - endorsed by Org2${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg2.createTransaction('VerifyAssetProperties');
transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string)
});
const verifyResultBuffer = await transaction.evaluate(assetKey);
if (verifyResultBuffer) {
const verifyResult = Boolean(verifyResultBuffer.toString());
if (verifyResult) {
console.log(`*** Successfully VerifyAssetProperties, private information about asset ${assetKey} has been verified by Org2`);
} else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetKey} has not been verified by Org2`);
}
} else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetKey} has not been verified by Org2`);
}
} catch (verifyError) {
console.log(`${RED}*** Failed: VerifyAssetProperties - ${verifyError}${RESET}`);
}
try {
// Agree to a buy by Org2
const asset_price = {
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
const asset_properties = {
object_type: 'asset_properties',
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetKey} as Org2 - endorsed by Org2${RESET}`);
transaction = contractOrg2.createTransaction('AgreeToBuy');
transaction.setEndorsingOrganizations(org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string),
asset_properties: Buffer.from(asset_properties_string)
});
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org2 has agreed to buy asset ${assetKey} for 100`);
} catch (buyError) {
console.log(`${RED}*** Failed: AgreeToBuy - ${buyError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2);
// Org1 should be able to read the private data details of this asset
await readPrivateAsset(assetKey, org1, contractOrg1);
// Org2 is not the owner and does not have the private details, this should fail
await readPrivateAsset(assetKey, org2, contractOrg2);
// Org1 should be able to read the sale price of this asset
await readSalePrice(assetKey, org1, contractOrg1);
// Org2 has not set a sale price and this should fail
await readSalePrice(assetKey, org2, contractOrg2);
// Org1 has not agreed to buy so this should fail
await readBidPrice(assetKey, org1, contractOrg1);
// Org2 should be able to see the price it has agreed
await readBidPrice(assetKey, org2, contractOrg2);
try {
// Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price
// are not the same
const asset_price = {
asset_id: assetKey.toString(),
price: 110,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
console.log(`${RED}*** Failed: committed, TransferAsset should have failed for asset ${assetKey}${RESET}`);
} catch (transferError) {
console.log(`*** Success: TransferAsset - ${transferError}`);
}
try {
// Agree to a sell by Org1
// Org1, the seller will agree to the bid price of Org2
const asset_price = {
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('AgreeToSell');
transaction.setEndorsingOrganizations(org1);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`);
} catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org1, contractOrg1, contractOrg2);
// Org1 should be able to read the private data details of this asset
await readPrivateAsset(assetKey, org1, contractOrg1);
// Org1 should be able to read the sale price of this asset
await readSalePrice(assetKey, org1, contractOrg1);
// Org2 should be able to see the price it has agreed
await readBidPrice(assetKey, org2, contractOrg2);
try {
// Org2 user will try to transfer the asset to Org2
// This will fail as the owner is Org1
const asset_price = {
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org2 - endorsed by Org1${RESET}`);
transaction = contractOrg2.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
console.log(`${RED}*** FAILED: committed, TransferAsset - Org2 now owns the asset ${assetKey}${RESET}`);
} catch (transferError) {
console.log(`*** Succeded: TransferAsset - ${transferError}`);
}
try {
// Org1 will transfer the asset to Org2
// This will now complete as the sell price and the bid price are the same
const asset_price = {
asset_id: assetKey.toString(),
price: 100,
trade_id: randomNumber.toString()
};
const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({
asset_price: Buffer.from(asset_price_string)
});
await transaction.submit(assetKey, org2);
console.log(`*** Results: committed, TransferAsset - Org2 now owns the asset ${assetKey}`);
} catch (transferError) {
console.log(`${RED}*** Failed: TransferAsset - ${transferError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org2, contractOrg1, contractOrg2);
// Org2 should be able to read the private data details of this asset
await readPrivateAsset(assetKey, org2, contractOrg2);
// Org1 should not be able to read the private data details of this asset
await readPrivateAsset(assetKey, org1, contractOrg1);
try {
// This is an update to the public state and requires only the owner to endorse.
// Org2 wants to indicate that the items is no longer for sale
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${assetKey}, as Org2 - endorse by Org2${RESET}`);
transaction = contractOrg2.createTransaction('ChangePublicDescription');
transaction.setEndorsingOrganizations(org2);
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org2} is NOT for sale`);
console.log('*** Results: committed - Org2 is now the owner and asset is not for sale');
} catch (updateError) {
console.log(`${RED}*** Failed: ChangePublicDescription has failed by Org2 - ${updateError}${RESET}`);
}
// read the public details by both orgs
await readAssetByBothOrgs(assetKey, org2, contractOrg1, contractOrg2);
} catch (runError) {
console.error(`Error in transaction: ${runError}`);
if (runError.stack) {
console.error(runError.stack);
}
process.exit(1);
} finally {
// Disconnect from the gateway peer when all work for this client identity is complete
console.log(`${GREEN}--> Close gateways`);
gatewayOrg1.disconnect();
gatewayOrg2.disconnect();
}
} catch (error) {
console.error(`Error in setup: ${error}`);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
console.log(`${GREEN} **** END ****${RESET}`);
}
main();

View file

@ -0,0 +1,22 @@
{
"name": "asset-transfer-secured-agreement",
"version": "1.0.0",
"description": "Javascript application that uses implicit private data collections, state-based endorsement, and organization-based ownership and access control to keep data private and securely transfer an asset with the consent of both the current owner and buyer",
"engines": {
"node": ">=12",
"npm": ">=5"
},
"engineStrict": true,
"author": "Hyperledger",
"license": "Apache-2.0",
"scripts": {
"lint": "eslint *.js"
},
"dependencies": {
"fabric-ca-client": "^2.2.19",
"fabric-network": "^2.2.19"
},
"devDependencies": {
"eslint": "^7.32.0"
}
}

View file

@ -0,0 +1 @@
[Secured asset transfer in Fabric Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html)

View file

@ -0,0 +1,624 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/hyperledger/fabric-chaincode-go/pkg/statebased"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
const (
typeAssetForSale = "S"
typeAssetBid = "B"
typeAssetSaleReceipt = "SR"
typeAssetBuyReceipt = "BR"
)
type SmartContract struct {
contractapi.Contract
}
// Asset struct and properties must be exported (start with capitals) to work with contract api metadata
type Asset struct {
ObjectType string `json:"objectType"` // ObjectType is used to distinguish different object types in the same chaincode namespace
ID string `json:"assetID"`
OwnerOrg string `json:"ownerOrg"`
PublicDescription string `json:"publicDescription"`
}
type receipt struct {
price int
timestamp time.Time
}
// CreateAsset creates an asset, sets it as owned by the client's org and returns its id
// the id of the asset corresponds to the hash of the properties of the asset that are passed by transiet field
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, publicDescription string) (string, error) {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return "", fmt.Errorf("error getting transient: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return "", fmt.Errorf("asset_properties key not found in the transient map")
}
// AssetID will be the hash of the asset's properties
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
// Get the clientOrgId from the input, will be used for implicit collection, owner, and state-based endorsement policy
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", err
}
// In this scenario, client is only authorized to read/write private data from its own peer, therefore verify client org id matches peer org id.
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
asset := Asset{
ObjectType: "asset",
ID: assetID,
OwnerOrg: clientOrgID,
PublicDescription: publicDescription,
}
assetBytes, err := json.Marshal(asset)
if err != nil {
return "", fmt.Errorf("failed to create asset JSON: %v", err)
}
err = ctx.GetStub().PutState(assetID, assetBytes)
if err != nil {
return "", fmt.Errorf("failed to put asset in public data: %v", err)
}
// Set the endorsement policy such that an owner org peer is required to endorse future updates.
// In practice, consider additional endorsers such as a trusted third party to further secure transfers.
endorsingOrgs := []string{clientOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return "", fmt.Errorf("failed setting state based endorsement for buyer and seller: %v", err)
}
// Persist private immutable asset properties to owner's private data collection
collection := buildCollectionName(clientOrgID)
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
if err != nil {
return "", fmt.Errorf("failed to put Asset private details: %v", err)
}
return assetID, nil
}
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description
func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return fmt.Errorf("failed to get asset: %v", err)
}
// Auth check to ensure that client's org actually owns the asset
if clientOrgID != asset.OwnerOrg {
return fmt.Errorf("a client from %s cannot update the description of a asset owned by %s", clientOrgID, asset.OwnerOrg)
}
asset.PublicDescription = newDescription
updatedAssetJSON, err := json.Marshal(asset)
if err != nil {
return fmt.Errorf("failed to marshal asset: %v", err)
}
return ctx.GetStub().PutState(assetID, updatedAssetJSON)
}
// AgreeToSell adds seller's asking price to seller's implicit private data collection.
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error {
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return err
}
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
// Verify that this client belongs to the peer's org
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return err
}
// Verify that this clientOrgId actually owns the asset.
if clientOrgID != asset.OwnerOrg {
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
}
return agreeToPrice(ctx, assetID, typeAssetForSale)
}
// AgreeToBuy adds buyer's bid price and asset properties to buyer's implicit private data collection
func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, assetID string) error {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
}
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
// Verify that this client belongs to the peer's org
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return err
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return fmt.Errorf("asset_properties key not found in the transient map")
}
// Persist private immutable asset properties to seller's private data collection
collection := buildCollectionName(clientOrgID)
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
if err != nil {
return fmt.Errorf("failed to put Asset private details: %v", err)
}
return agreeToPrice(ctx, assetID, typeAssetBid)
}
// agreeToPrice adds a bid or ask price to caller's implicit private data collection
func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error {
// In this scenario, both buyer and seller are authoried to read/write private about transfer after seller agrees to sell.
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
transMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient: %v", err)
}
// Asset price must be retrieved from the transient field as they are private
price, ok := transMap["asset_price"]
if !ok {
return fmt.Errorf("asset_price key not found in the transient map")
}
collection := buildCollectionName(clientOrgID)
// Persist the agreed to price in a collection sub-namespace based on priceType key prefix,
// to avoid collisions between private asset properties, sell price, and buy price
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID})
if err != nil {
return fmt.Errorf("failed to create composite key: %v", err)
}
// The Price hash will be verified later, therefore always pass and persist price bytes as is,
// so that there is no risk of nondeterministic marshaling.
err = ctx.GetStub().PutPrivateData(collection, assetPriceKey, price)
if err != nil {
return fmt.Errorf("failed to put asset bid: %v", err)
}
return nil
}
// VerifyAssetProperties allows a buyer to validate the properties of
// an asset they intend to buy against the owner's implicit private data collection
// and verifies that the asset properties never changed from the origin of the asset by checking their hash against the assetID
func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
transMap, err := ctx.GetStub().GetTransient()
if err != nil {
return false, fmt.Errorf("error getting transient: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transMap["asset_properties"]
if !ok {
return false, fmt.Errorf("asset_properties key not found in the transient map")
}
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return false, fmt.Errorf("failed to get asset: %v", err)
}
collectionOwner := buildCollectionName(asset.OwnerOrg)
immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID)
if err != nil {
return false, fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
}
if immutablePropertiesOnChainHash == nil {
return false, fmt.Errorf("asset private properties hash does not exist: %s", assetID)
}
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
calculatedPropertiesHash := hash.Sum(nil)
// verify that the hash of the passed immutable properties matches the on-chain hash
if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) {
return false, fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x",
calculatedPropertiesHash,
immutablePropertiesJSON,
immutablePropertiesOnChainHash,
)
}
// verify that the hash of the passed immutable properties and on chain hash matches the assetID
if !(hex.EncodeToString(immutablePropertiesOnChainHash) == assetID) {
return false, fmt.Errorf("hash %x for passed immutable properties %s does match on-chain hash %x but do not match assetID %s: asset was altered from its initial form",
calculatedPropertiesHash,
immutablePropertiesJSON,
immutablePropertiesOnChainHash,
assetID)
}
return true, nil
}
// TransferAsset checks transfer conditions and then transfers asset state to buyer.
// TransferAsset can only be called by current owner
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return err
}
transMap, err := ctx.GetStub().GetTransient()
if err != nil {
return fmt.Errorf("error getting transient data: %v", err)
}
priceJSON, ok := transMap["asset_price"]
if !ok {
return fmt.Errorf("asset_price key not found in the transient map")
}
var agreement Agreement
err = json.Unmarshal(priceJSON, &agreement)
if err != nil {
return fmt.Errorf("failed to unmarshal price JSON: %v", err)
}
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return fmt.Errorf("failed to get asset: %v", err)
}
err = verifyTransferConditions(ctx, asset, clientOrgID, buyerOrgID, priceJSON)
if err != nil {
return fmt.Errorf("failed transfer verification: %v", err)
}
err = transferAssetState(ctx, asset, clientOrgID, buyerOrgID, agreement.Price)
if err != nil {
return fmt.Errorf("failed asset transfer: %v", err)
}
return nil
}
// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price
func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
asset *Asset,
clientOrgID string,
buyerOrgID string,
priceJSON []byte) error {
// CHECK1: Auth check to ensure that client's org actually owns the asset
if clientOrgID != asset.OwnerOrg {
return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg)
}
// CHECK2: Verify that buyer and seller on-chain asset defintion hash matches
collectionSeller := buildCollectionName(clientOrgID)
collectionBuyer := buildCollectionName(buyerOrgID)
sellerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
if err != nil {
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
}
if sellerPropertiesOnChainHash == nil {
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
}
buyerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, asset.ID)
if err != nil {
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
}
if buyerPropertiesOnChainHash == nil {
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
}
// verify that buyer and seller on-chain asset defintion hash matches
if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
sellerPropertiesOnChainHash,
buyerPropertiesOnChainHash,
)
}
// CHECK3: Verify that seller and buyer agreed on the same price
// Get sellers asking price
assetForSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key: %v", err)
}
sellerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, assetForSaleKey)
if err != nil {
return fmt.Errorf("failed to get seller price hash: %v", err)
}
if sellerPriceHash == nil {
return fmt.Errorf("seller price for %s does not exist", asset.ID)
}
// Get buyers bid price
assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key: %v", err)
}
buyerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetBidKey)
if err != nil {
return fmt.Errorf("failed to get buyer price hash: %v", err)
}
if buyerPriceHash == nil {
return fmt.Errorf("buyer price for %s does not exist", asset.ID)
}
hash := sha256.New()
hash.Write(priceJSON)
calculatedPriceHash := hash.Sum(nil)
// Verify that the hash of the passed price matches the on-chain sellers price hash
if !bytes.Equal(calculatedPriceHash, sellerPriceHash) {
return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, seller hasn't agreed to the passed trade id and price",
calculatedPriceHash,
priceJSON,
sellerPriceHash,
)
}
// Verify that the hash of the passed price matches the on-chain buyer price hash
if !bytes.Equal(calculatedPriceHash, buyerPriceHash) {
return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, buyer hasn't agreed to the passed trade id and price",
calculatedPriceHash,
priceJSON,
buyerPriceHash,
)
}
return nil
}
// transferAssetState performs the public and private state updates for the transferred asset
// changes the endorsement for the transferred asset sbe to the new owner org
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, clientOrgID string, buyerOrgID string, price int) error {
// Update ownership in public state
asset.OwnerOrg = buyerOrgID
updatedAsset, err := json.Marshal(asset)
if err != nil {
return err
}
err = ctx.GetStub().PutState(asset.ID, updatedAsset)
if err != nil {
return fmt.Errorf("failed to write asset for buyer: %v", err)
}
// Changes the endorsement policy to the new owner org
endorsingOrgs := []string{buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
}
// Delete asset description from seller collection
collectionSeller := buildCollectionName(clientOrgID)
err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID)
if err != nil {
return fmt.Errorf("failed to delete Asset private details from seller: %v", err)
}
// Delete the price records for seller
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key for seller: %v", err)
}
err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey)
if err != nil {
return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err)
}
// Delete the price records for buyer
collectionBuyer := buildCollectionName(buyerOrgID)
assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key for buyer: %v", err)
}
err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey)
if err != nil {
return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err)
}
// Keep record for a 'receipt' in both buyers and sellers private data collection to record the sale price and date.
// Persist the agreed to price in a collection sub-namespace based on receipt key prefix.
receiptBuyKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBuyReceipt, []string{asset.ID, ctx.GetStub().GetTxID()})
if err != nil {
return fmt.Errorf("failed to create composite key for receipt: %v", err)
}
txTimestamp, err := ctx.GetStub().GetTxTimestamp()
if err != nil {
return fmt.Errorf("failed to create timestamp for receipt: %v", err)
}
timestamp, err := ptypes.Timestamp(txTimestamp)
if err != nil {
return err
}
assetReceipt := receipt{
price: price,
timestamp: timestamp,
}
receipt, err := json.Marshal(assetReceipt)
if err != nil {
return fmt.Errorf("failed to marshal receipt: %v", err)
}
err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receipt)
if err != nil {
return fmt.Errorf("failed to put private asset receipt for buyer: %v", err)
}
receiptSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetSaleReceipt, []string{ctx.GetStub().GetTxID(), asset.ID})
if err != nil {
return fmt.Errorf("failed to create composite key for receipt: %v", err)
}
err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receipt)
if err != nil {
return fmt.Errorf("failed to put private asset receipt for seller: %v", err)
}
return nil
}
// getClientOrgID gets the client org ID.
func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
if err != nil {
return "", fmt.Errorf("failed getting client's orgID: %v", err)
}
return clientOrgID, nil
}
// getClientImplicitCollectionNameAndVerifyClientOrg gets the implicit collection for the client and checks that the client is from the same org as the peer
func getClientImplicitCollectionNameAndVerifyClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := getClientOrgID(ctx)
if err != nil {
return "", err
}
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
return buildCollectionName(clientOrgID), nil
}
// verifyClientOrgMatchesPeerOrg checks that the client is from the same org as the peer
func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
peerOrgID, err := shim.GetMSPID()
if err != nil {
return fmt.Errorf("failed getting peer's orgID: %v", err)
}
if clientOrgID != peerOrgID {
return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer",
clientOrgID,
peerOrgID,
)
}
return nil
}
// buildCollectionName returns the implicit collection name for an org
func buildCollectionName(clientOrgID string) string {
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
}
// setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
endorsementPolicy, err := statebased.NewStateEP(nil)
if err != nil {
return err
}
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgsToEndorse...)
if err != nil {
return fmt.Errorf("failed to add org to endorsement policy: %v", err)
}
policy, err := endorsementPolicy.Policy()
if err != nil {
return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err)
}
err = ctx.GetStub().SetStateValidationParameter(assetID, policy)
if err != nil {
return fmt.Errorf("failed to set validation parameter on asset: %v", err)
}
return nil
}
// GetAssetHashId allows a potential buyer to validate the properties of an asset against the asset Id hash on chain and returns the hash
func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) {
transientMap, err := ctx.GetStub().GetTransient()
if err != nil {
return "", fmt.Errorf("error getting transient: %v", err)
}
// Asset properties must be retrieved from the transient field as they are private
propertiesJSON, ok := transientMap["asset_properties"]
if !ok {
return "", fmt.Errorf("asset_properties key not found in the transient map")
}
hash := sha256.New()
hash.Write(propertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
asset, err := s.ReadAsset(ctx, assetID)
if err != nil {
return "", fmt.Errorf("failed to get asset: %v, asset properies provided do not represent any on chain asset", err)
}
if asset.ID != assetID {
return "", fmt.Errorf("Asset properies provided do not correpond to any on chain asset")
}
return asset.ID, nil
}
func main() {
chaincode, err := contractapi.NewChaincode(new(SmartContract))
if err != nil {
log.Panicf("Error create transfer asset chaincode: %v", err)
}
if err := chaincode.Start(); err != nil {
log.Panicf("Error starting asset chaincode: %v", err)
}
}

View file

@ -0,0 +1,177 @@
/*
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// QueryResult structure used for handling result of query
type QueryResult struct {
Record *Asset
TxId string `json:"txId"`
Timestamp time.Time `json:"timestamp"`
}
type Agreement struct {
ID string `json:"asset_id"`
Price int `json:"price"`
TradeID string `json:"trade_id"`
}
// ReadAsset returns the public asset data
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) {
// Since only public data is accessed in this function, no access control is required
assetJSON, err := ctx.GetStub().GetState(assetID)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("%s does not exist", assetID)
}
var asset *Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return asset, nil
}
// GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection
func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return "", err
}
immutableProperties, err := ctx.GetStub().GetPrivateData(collection, assetID)
if err != nil {
return "", fmt.Errorf("failed to read asset private properties from client org's collection: %v", err)
}
if immutableProperties == nil {
return "", fmt.Errorf("asset private details does not exist in client org's collection: %s", assetID)
}
return string(immutableProperties), nil
}
// GetAssetSalesPrice returns the sales price
func (s *SmartContract) GetAssetSalesPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
return getAssetPrice(ctx, assetID, typeAssetForSale)
}
// GetAssetBidPrice returns the bid price
func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
return getAssetPrice(ctx, assetID, typeAssetBid)
}
// getAssetPrice gets the bid or ask price from caller's implicit private data collection
func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) (string, error) {
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return "", err
}
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID})
if err != nil {
return "", fmt.Errorf("failed to create composite key: %v", err)
}
price, err := ctx.GetStub().GetPrivateData(collection, assetPriceKey)
if err != nil {
return "", fmt.Errorf("failed to read asset price from implicit private data collection: %v", err)
}
if price == nil {
return "", fmt.Errorf("asset price does not exist: %s", assetID)
}
return string(price), nil
}
// QueryAssetSaleAgreements returns all of an organization's proposed sales
func (s *SmartContract) QueryAssetSaleAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) {
return queryAgreementsByType(ctx, typeAssetForSale)
}
// QueryAssetBuyAgreements returns all of an organization's proposed bids
func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) {
return queryAgreementsByType(ctx, typeAssetBid)
}
func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeType string) ([]Agreement, error) {
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
if err != nil {
return nil, err
}
// Query for any object type starting with `agreeType`
agreementsIterator, err := ctx.GetStub().GetPrivateDataByPartialCompositeKey(collection, agreeType, []string{})
if err != nil {
return nil, fmt.Errorf("failed to read from private data collection: %v", err)
}
defer agreementsIterator.Close()
var agreements []Agreement
for agreementsIterator.HasNext() {
resp, err := agreementsIterator.Next()
if err != nil {
return nil, err
}
var agreement Agreement
err = json.Unmarshal(resp.Value, &agreement)
if err != nil {
return nil, err
}
agreements = append(agreements, agreement)
}
return agreements, nil
}
// QueryAssetHistory returns the chain of custody for a asset since issuance
func (s *SmartContract) QueryAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]QueryResult, error) {
resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var results []QueryResult
for resultsIterator.HasNext() {
response, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset *Asset
err = json.Unmarshal(response.Value, &asset)
if err != nil {
return nil, err
}
timestamp, err := ptypes.Timestamp(response.Timestamp)
if err != nil {
return nil, err
}
record := QueryResult{
TxId: response.TxId,
Timestamp: timestamp,
Record: asset,
}
results = append(results, record)
}
return results, nil
}

View file

@ -0,0 +1,34 @@
module github.com/hyperledger/fabric-samples/chaincode/tradingMarbles
go 1.17
require (
github.com/golang/protobuf v1.5.2
github.com/hyperledger/fabric-chaincode-go v0.0.0-20230228194215-b84622ba6a7a
github.com/hyperledger/fabric-contract-api-go v1.2.1
)
require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.8 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gobuffalo/envy v1.10.1 // indirect
github.com/gobuffalo/packd v1.0.1 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/hyperledger/fabric-protos-go v0.3.0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

File diff suppressed because it is too large Load diff