mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 07:25:10 +00:00
Copy asset-transfer-secured-agreement to adapt to Varion requirements
This commit is contained in:
parent
33f176917a
commit
be5d9fd6e2
19 changed files with 3540 additions and 0 deletions
65
varion-contract/README.md
Normal file
65
varion-contract/README.md
Normal 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
|
||||
```
|
||||
102
varion-contract/application-gateway-typescript/.eslintrc.json
Normal file
102
varion-contract/application-gateway-typescript/.eslintrc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
varion-contract/application-gateway-typescript/.gitignore
vendored
Normal file
14
varion-contract/application-gateway-typescript/.gitignore
vendored
Normal 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
|
||||
33
varion-contract/application-gateway-typescript/package.json
Normal file
33
varion-contract/application-gateway-typescript/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
215
varion-contract/application-gateway-typescript/src/app.ts
Normal file
215
varion-contract/application-gateway-typescript/src/app.ts
Normal 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;
|
||||
});
|
||||
107
varion-contract/application-gateway-typescript/src/connect.ts
Normal file
107
varion-contract/application-gateway-typescript/src/connect.ts
Normal 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]);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
13
varion-contract/application-gateway-typescript/src/utils.ts
Normal file
13
varion-contract/application-gateway-typescript/src/utils.ts
Normal 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);
|
||||
}
|
||||
17
varion-contract/application-gateway-typescript/tsconfig.json
Normal file
17
varion-contract/application-gateway-typescript/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
5
varion-contract/application-javascript/.eslintignore
Normal file
5
varion-contract/application-javascript/.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
37
varion-contract/application-javascript/.eslintrc.js
Normal file
37
varion-contract/application-javascript/.eslintrc.js
Normal 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']
|
||||
}
|
||||
};
|
||||
14
varion-contract/application-javascript/.gitignore
vendored
Normal file
14
varion-contract/application-javascript/.gitignore
vendored
Normal 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
|
||||
544
varion-contract/application-javascript/app.js
Normal file
544
varion-contract/application-javascript/app.js
Normal 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();
|
||||
22
varion-contract/application-javascript/package.json
Normal file
22
varion-contract/application-javascript/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
varion-contract/chaincode-go/README.md
Normal file
1
varion-contract/chaincode-go/README.md
Normal 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)
|
||||
624
varion-contract/chaincode-go/asset_transfer.go
Normal file
624
varion-contract/chaincode-go/asset_transfer.go
Normal 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)
|
||||
}
|
||||
}
|
||||
177
varion-contract/chaincode-go/asset_transfer_queries.go
Normal file
177
varion-contract/chaincode-go/asset_transfer_queries.go
Normal 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
|
||||
}
|
||||
34
varion-contract/chaincode-go/go.mod
Normal file
34
varion-contract/chaincode-go/go.mod
Normal 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
|
||||
)
|
||||
1240
varion-contract/chaincode-go/go.sum
Normal file
1240
varion-contract/chaincode-go/go.sum
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue