updated and improved asset-transfer-secured-agreement sample (#751)

Signed-off-by: fraVlaca <ocsenarf@outlook.com>
This commit is contained in:
fraVlaca 2022-05-26 13:42:01 +01:00 committed by GitHub
parent 4681fe7865
commit 94867dd517
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 203 additions and 160 deletions

View file

@ -24,6 +24,7 @@ The smart contract (in folder `chaincode-go`) implements the following functions
- GetAssetPrivateProperties - GetAssetPrivateProperties
- GetAssetSalesPrice - GetAssetSalesPrice
- GetAssetBidPrice - GetAssetBidPrice
- GetAssetHashId
- QueryAssetSaleAgreements - QueryAssetSaleAgreements
- QueryAssetBuyAgreements - QueryAssetBuyAgreements
- QueryAssetHistory - QueryAssetHistory

View file

@ -15,7 +15,7 @@ const chaincodeName = 'secured';
//Use a random key so that we can run multiple times //Use a random key so that we can run multiple times
const now = Date.now().toString(); const now = Date.now().toString();
const assetKey = `asset${now}`; let assetKey: string;
async function main(): Promise<void> { async function main(): Promise<void> {
@ -57,9 +57,8 @@ async function main(): Promise<void> {
const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2); const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2);
// Create an asset by organization Org1, this only requires the owning organization to endorse. // Create an asset by organization Org1, this only requires the owning organization to endorse.
await contractWrapperOrg1.createAsset({ assetId: assetKey, assetKey = await contractWrapperOrg1.createAsset(mspIdOrg1,
ownerOrg: mspIdOrg1, `Asset owned by ${mspIdOrg1} is not for sale`, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is not for sale`}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Read the public details by org1. // Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1); await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
@ -109,16 +108,16 @@ async function main(): Promise<void> {
assetId: assetKey, assetId: assetKey,
price: 110, price: 110,
tradeId: now, tradeId: now,
}); }, mspIdOrg2);
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details, // 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. // so the hash of the details may be checked by the chaincode.
await contractWrapperOrg2.verifyAssetProperties({ assetId:assetKey, color:'blue', size:35}); await contractWrapperOrg2.verifyAssetProperties(assetKey, {color:'blue', size:35});
// Agree to a buy by org2. // Agree to a buy by org2.
await contractWrapperOrg2.agreeToBuy( {assetId: assetKey, await contractWrapperOrg2.agreeToBuy( {assetId: assetKey,
price: 100, price: 100,
tradeId: now}); tradeId: now}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
// Org1 should be able to read the sale price of this asset. // Org1 should be able to read the sale price of this asset.
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1); await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
@ -142,12 +141,12 @@ async function main(): Promise<void> {
// Org1 will try to transfer the asset to Org2 // 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. // This will fail due to the sell price and the bid price are not the same.
try{ try{
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, mspIdOrg1, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`); console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
} }
// Agree to a sell by Org1, the seller will agree to the bid price of Org2. // Agree to a sell by Org1, the seller will agree to the bid price of Org2.
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}); await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now}, mspIdOrg2);
// Read the public details by org1. // Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1); await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
@ -167,14 +166,14 @@ async function main(): Promise<void> {
// Org2 user will try to transfer the asset to Org1. // Org2 user will try to transfer the asset to Org1.
// This will fail as the owner is Org1. // This will fail as the owner is Org1.
try{ try{
await contractWrapperOrg2.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
} catch(e) { } catch(e) {
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`); console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
} }
// Org1 will transfer the asset to Org2. // Org1 will transfer the asset to Org2.
// This will now complete as the sell price and the bid price are the same. // This will now complete as the sell price and the bid price are the same.
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2); await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, mspIdOrg1, mspIdOrg2);
// Read the public details by org1. // Read the public details by org1.
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2); await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);

View file

@ -7,6 +7,7 @@ import { Contract } from '@hyperledger/fabric-gateway';
import { TextDecoder } from 'util'; import { TextDecoder } from 'util';
import { GREEN, parse, RED, RESET } from './utils'; import { GREEN, parse, RED, RESET } from './utils';
import crpto from 'crypto'; import crpto from 'crypto';
import { mspIdOrg2 } from './connect';
const randomBytes = crpto.randomBytes(256).toString('hex'); const randomBytes = crpto.randomBytes(256).toString('hex');
@ -19,7 +20,6 @@ interface AssetJSON {
interface AssetPropertiesJSON { interface AssetPropertiesJSON {
objectType: string; objectType: string;
assetID: string;
color: string; color: string;
size: number; size: number;
salt: string; salt: string;
@ -44,7 +44,6 @@ export interface Asset {
} }
export interface AssetProperties { export interface AssetProperties {
assetId: string;
color: string; color: string;
size: number; size: number;
} }
@ -61,27 +60,30 @@ export class ContractWrapper {
readonly #org: string; readonly #org: string;
readonly #utf8Decoder = new TextDecoder(); readonly #utf8Decoder = new TextDecoder();
readonly #randomBytes: string = randomBytes; readonly #randomBytes: string = randomBytes;
#endorsingOrgs: { [id: string]: string[] };
public constructor(contract: Contract, org: string) { public constructor(contract: Contract, org: string) {
this.#contract = contract; this.#contract = contract;
this.#org = org; this.#org = org;
this.#endorsingOrgs = {};
} }
public async createAsset(asset: Asset, privateData: AssetPrivateData): Promise<void> { public async createAsset(ownerOrg: string, publicDescription: string, privateData: AssetPrivateData): Promise<string> {
console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${asset.assetId} as ${asset.ownerOrg} - endorsed by Org1.${RESET}`); console.log(`${GREEN}--> Submit Transaction: CreateAsset as ${ownerOrg} - endorsed by Org1.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = { const assetPropertiesJSON: AssetPropertiesJSON = {
objectType: 'asset_properties', objectType: 'asset_properties',
assetID: asset.assetId,
color: privateData.Color, color: privateData.Color,
size: privateData.Size, size: privateData.Size,
salt: this.#randomBytes }; salt: this.#randomBytes };
await this.#contract.submit('CreateAsset', { const resultBytes = await this.#contract.submit('CreateAsset', {
arguments: [asset.assetId, asset.publicDescription], arguments: [publicDescription],
transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)}, transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)},
}); });
const assetID = this.#utf8Decoder.decode(resultBytes);
console.log(`*** Result: committed, asset ${asset.assetId} is owned by Org1`); 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> { public async readAsset(assetKey: string, ownerOrg: string): Promise<void> {
@ -113,7 +115,6 @@ export class ContractWrapper {
const resultString = this.#utf8Decoder.decode(resultBytes); const resultString = this.#utf8Decoder.decode(resultBytes);
const json = parse<AssetPropertiesJSON>(resultString); const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = { const result: AssetProperties = {
assetId: json.assetID,
color: json.color, color: json.color,
size: json.size, size: json.size,
}; };
@ -129,12 +130,13 @@ export class ContractWrapper {
await this.#contract.submit('ChangePublicDescription', { await this.#contract.submit('ChangePublicDescription', {
arguments:[asset.assetId, asset.publicDescription], arguments:[asset.assetId, asset.publicDescription],
endorsingOrganizations: this.#endorsingOrgs[asset.assetId]
}); });
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`); console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
} }
public async agreeToSell(assetPrice: AssetPrice): Promise<void> { public async agreeToSell(assetPrice: AssetPrice, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`); console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = { const assetPriceJSON: AssetPriceJSON = {
@ -144,23 +146,28 @@ export class ContractWrapper {
}; };
await this.#contract.submit('AgreeToSell', { await this.#contract.submit('AgreeToSell', {
arguments:[assetPrice.assetId], arguments:[assetPrice.assetId, buyerOrgID],
transientData: {asset_price: JSON.stringify(assetPriceJSON)} transientData: {asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
}); });
//update local record of sbe to inlcude buyer org if not already
if (this.#endorsingOrgs[assetPrice.assetId].indexOf('buyerOrgID') == -1){
this.#endorsingOrgs[assetPrice.assetId].push(buyerOrgID);
}
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`); console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`);
} }
public async verifyAssetProperties(assetProperties: AssetProperties): Promise<void> { public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetProperties.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`); console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties', const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
assetID: assetProperties.assetId,
color: assetProperties.color, color: assetProperties.color,
size: assetProperties.size, size: assetProperties.size,
salt: this.#randomBytes }; salt: this.#randomBytes };
const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', { const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', {
arguments:[assetPropertiesJSON.assetID], arguments:[assetId],
transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)}, transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)},
}); });
@ -168,24 +175,29 @@ export class ContractWrapper {
if (resultString.length !== 0) { if (resultString.length !== 0) {
const json = parse<AssetPropertiesJSON>(resultString); const json = parse<AssetPropertiesJSON>(resultString);
const result: AssetProperties = { const result: AssetProperties = {
assetId: json.assetID,
color: json.color, color: json.color,
size: json.size size: json.size
}; };
if (result) { if (result) {
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetProperties.assetId} has been verified by ${this.#org}`); console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
} else { } else {
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`); console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
} }
} else { } else {
throw new Error(`Private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`); throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
} }
} }
public async agreeToBuy(assetPrice: AssetPrice, ): Promise<void> { 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 };
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
const assetPriceJSON: AssetPriceJSON = { const assetPriceJSON: AssetPriceJSON = {
assetID: assetPrice.assetId, assetID: assetPrice.assetId,
price: assetPrice.price, price: assetPrice.price,
@ -194,7 +206,11 @@ export class ContractWrapper {
await this.#contract.submit('AgreeToBuy', { await this.#contract.submit('AgreeToBuy', {
arguments:[assetPrice.assetId], arguments:[assetPrice.assetId],
transientData: {asset_price: JSON.stringify(assetPriceJSON)} 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`); console.log(`*** Result: committed, ${this.#org} has agreed to buy asset ${assetPrice.assetId} for 100`);
@ -242,9 +258,9 @@ export class ContractWrapper {
console.log('*** Result: GetAssetBidPrice', result); console.log('*** Result: GetAssetBidPrice', result);
} }
public async transferAsset( privateData: AssetPrivateData, assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> { public async transferAsset(assetPrice: AssetPrice, ownerOrgID: string, buyerOrgID: string): Promise<void> {
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org}.${RESET}`); console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`);
if (this.#org !== ownerOrgID) { if (this.#org !== ownerOrgID) {
console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`); console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`);
@ -252,20 +268,12 @@ export class ContractWrapper {
console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`); console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`);
} }
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
assetID: assetPrice.assetId,
color: privateData.Color,
size: privateData.Size,
salt: this.#randomBytes };
const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId}; const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId};
await this.#contract.submit('TransferAsset', { await this.#contract.submit('TransferAsset', {
arguments:[assetPropertiesJSON.assetID, buyerOrgID], arguments:[assetPrice.assetId, buyerOrgID],
transientData: { transientData: { asset_price: JSON.stringify(assetPriceJSON) },
asset_properties: JSON.stringify(assetPropertiesJSON), endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
asset_price: JSON.stringify(assetPriceJSON)},
endorsingOrganizations:endorsingOrganizations
}); });
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`); console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);

View file

@ -207,8 +207,7 @@ async function main() {
console.log(`${GREEN} **** START ****${RESET}`); console.log(`${GREEN} **** START ****${RESET}`);
try { try {
const randomNumber = Math.floor(Math.random() * 100) + 1; const randomNumber = Math.floor(Math.random() * 100) + 1;
// use a random key so that we can run multiple times let assetKey;
const assetKey = `asset-${randomNumber}`;
/** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */ /** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */
const gatewayOrg1 = await initGatewayForOrg1(); const gatewayOrg1 = await initGatewayForOrg1();
@ -231,20 +230,19 @@ async function main() {
// the actual peers that may be active at any given time. // the actual peers that may be active at any given time.
const asset_properties = { const asset_properties = {
object_type: 'asset_properties', object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue', color: 'blue',
size: 35, size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex') salt: Buffer.from(randomNumber.toString()).toString('hex')
}; };
const asset_properties_string = JSON.stringify(asset_properties); const asset_properties_string = JSON.stringify(asset_properties);
console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); console.log(`${GREEN}--> Submit Transaction: CreateAsset as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`); console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('CreateAsset'); transaction = contractOrg1.createTransaction('CreateAsset');
transaction.setEndorsingOrganizations(org1); transaction.setEndorsingOrganizations(org1);
transaction.setTransient({ transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string) asset_properties: Buffer.from(asset_properties_string)
}); });
await transaction.submit(assetKey, `Asset ${assetKey} owned by ${org1} is not for sale`); assetKey = await transaction.submit( `Asset owned by ${org1} is not for sale`);
console.log(`*** Result: committed, asset ${assetKey} is owned by Org1`); console.log(`*** Result: committed, asset ${assetKey} is owned by Org1`);
} catch (createError) { } catch (createError) {
console.log(`${RED}*** Failed: CreateAsset - ${createError}${RESET}`); console.log(`${RED}*** Failed: CreateAsset - ${createError}${RESET}`);
@ -314,7 +312,8 @@ async function main() {
transaction.setTransient({ transaction.setTransient({
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string)
}); });
await transaction.submit(assetKey); //call agree to sell with desired price and target buyer organization
await transaction.submit(assetKey, org2);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`); console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 110`);
} catch (sellError) { } catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
@ -326,7 +325,6 @@ async function main() {
// details may be checked by the chaincode. // details may be checked by the chaincode.
const asset_properties = { const asset_properties = {
object_type: 'asset_properties', object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue', color: 'blue',
size: 35, size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex') salt: Buffer.from(randomNumber.toString()).toString('hex')
@ -356,16 +354,24 @@ async function main() {
try { try {
// Agree to a buy by Org2 // Agree to a buy by Org2
const asset_price = { const asset_price = {
asset_id: assetKey, asset_id: assetKey.toString(),
price: 100, price: 100,
trade_id: randomNumber.toString() trade_id: randomNumber.toString()
}; };
const asset_price_string = JSON.stringify(asset_price); 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}`); console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetKey} as Org2 - endorsed by Org2${RESET}`);
transaction = contractOrg2.createTransaction('AgreeToBuy'); transaction = contractOrg2.createTransaction('AgreeToBuy');
transaction.setEndorsingOrganizations(org2); transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({ transaction.setTransient({
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string),
asset_properties: Buffer.from(asset_properties_string)
}); });
await transaction.submit(assetKey); await transaction.submit(assetKey);
console.log(`*** Result: committed, Org2 has agreed to buy asset ${assetKey} for 100`); console.log(`*** Result: committed, Org2 has agreed to buy asset ${assetKey} for 100`);
@ -395,14 +401,6 @@ async function main() {
// Org1 will try to transfer the asset to Org2 // Org1 will try to transfer the asset to Org2
// This will fail due to the sell price and the bid price // This will fail due to the sell price and the bid price
// are not the same // are not the same
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = { const asset_price = {
asset_id: assetKey, asset_id: assetKey,
price: 110, price: 110,
@ -411,11 +409,9 @@ async function main() {
const asset_price_string = JSON.stringify(asset_price); const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('TransferAsset'); transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1); transaction.setEndorsingOrganizations(org1);
transaction.setTransient({ transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string)
}); });
await transaction.submit(assetKey, org2); await transaction.submit(assetKey, org2);
@ -428,18 +424,18 @@ async function main() {
// Agree to a sell by Org1 // Agree to a sell by Org1
// Org1, the seller will agree to the bid price of Org2 // Org1, the seller will agree to the bid price of Org2
const asset_price = { const asset_price = {
asset_id: assetKey, asset_id: assetKey.toString(),
price: 100, price: 100,
trade_id: randomNumber.toString() trade_id: randomNumber.toString()
}; };
const asset_price_string = JSON.stringify(asset_price); const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`); console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
transaction = contractOrg1.createTransaction('AgreeToSell'); transaction = contractOrg1.createTransaction('AgreeToSell');
transaction.setEndorsingOrganizations(org1); transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({ transaction.setTransient({
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string)
}); });
await transaction.submit(assetKey); await transaction.submit(assetKey, org2);
console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`); console.log(`*** Result: committed, Org1 has agreed to sell asset ${assetKey} for 100`);
} catch (sellError) { } catch (sellError) {
console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`); console.log(`${RED}*** Failed: AgreeToSell - ${sellError}${RESET}`);
@ -460,27 +456,17 @@ async function main() {
try { try {
// Org2 user will try to transfer the asset to Org2 // Org2 user will try to transfer the asset to Org2
// This will fail as the owner is Org1 // This will fail as the owner is Org1
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = { const asset_price = {
asset_id: assetKey, asset_id: assetKey.toString(),
price: 100, price: 100,
trade_id: randomNumber.toString() trade_id: randomNumber.toString()
}; };
const asset_price_string = JSON.stringify(asset_price); const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org2 - endorsed by Org1${RESET}`); console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org2 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg2.createTransaction('TransferAsset'); transaction = contractOrg2.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2); transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({ transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string)
}); });
await transaction.submit(assetKey, org2); await transaction.submit(assetKey, org2);
@ -492,27 +478,18 @@ async function main() {
try { try {
// Org1 will transfer the asset to Org2 // Org1 will transfer the asset to Org2
// This will now complete as the sell price and the bid price are the same // This will now complete as the sell price and the bid price are the same
const asset_properties = {
object_type: 'asset_properties',
asset_id: assetKey,
color: 'blue',
size: 35,
salt: Buffer.from(randomNumber.toString()).toString('hex')
};
const asset_properties_string = JSON.stringify(asset_properties);
const asset_price = { const asset_price = {
asset_id: assetKey, asset_id: assetKey.toString(),
price: 100, price: 100,
trade_id: randomNumber.toString() trade_id: randomNumber.toString()
}; };
const asset_price_string = JSON.stringify(asset_price); const asset_price_string = JSON.stringify(asset_price);
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`); console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetKey} as Org1 - endorsed by Org1${RESET}`);
console.log(`${asset_properties_string}`);
transaction = contractOrg1.createTransaction('TransferAsset'); transaction = contractOrg1.createTransaction('TransferAsset');
transaction.setEndorsingOrganizations(org1, org2); transaction.setEndorsingOrganizations(org1, org2);
transaction.setTransient({ transaction.setTransient({
asset_properties: Buffer.from(asset_properties_string),
asset_price: Buffer.from(asset_price_string) asset_price: Buffer.from(asset_price_string)
}); });
await transaction.submit(assetKey, org2); await transaction.submit(assetKey, org2);

View file

@ -7,6 +7,7 @@ package main
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@ -42,24 +43,29 @@ type receipt struct {
timestamp time.Time timestamp time.Time
} }
// CreateAsset creates an asset and sets it as owned by the client's org // CreateAsset creates an asset, sets it as owned by the client's org and returns its id
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, publicDescription string) error { // 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() transientMap, err := ctx.GetStub().GetTransient()
if err != nil { if err != nil {
return fmt.Errorf("error getting transient: %v", err) return "", fmt.Errorf("error getting transient: %v", err)
} }
// Asset properties must be retrieved from the transient field as they are private // Asset properties must be retrieved from the transient field as they are private
immutablePropertiesJSON, ok := transientMap["asset_properties"] immutablePropertiesJSON, ok := transientMap["asset_properties"]
if !ok { if !ok {
return fmt.Errorf("asset_properties key not found in the transient map") return "", fmt.Errorf("asset_properties key not found in the transient map")
} }
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
assetID := hex.EncodeToString(hash.Sum(nil))
// Get client org id and verify it matches peer org id. // Get client org id and verify it matches peer org id.
// In this scenario, client is only authorized to read/write private data from its own peer. // In this scenario, client is only authorized to read/write private data from its own peer.
clientOrgID, err := getClientOrgID(ctx, true) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err) return "", fmt.Errorf("failed to get verified OrgID: %v", err)
} }
asset := Asset{ asset := Asset{
@ -70,34 +76,35 @@ func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
} }
assetBytes, err := json.Marshal(asset) assetBytes, err := json.Marshal(asset)
if err != nil { if err != nil {
return fmt.Errorf("failed to create asset JSON: %v", err) return "", fmt.Errorf("failed to create asset JSON: %v", err)
} }
err = ctx.GetStub().PutState(asset.ID, assetBytes) err = ctx.GetStub().PutState(assetID, assetBytes)
if err != nil { if err != nil {
return fmt.Errorf("failed to put asset in public data: %v", err) 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 // Set the endorsement policy such that an owner org peer is required to endorse future updates
err = setAssetStateBasedEndorsement(ctx, asset.ID, clientOrgID) endorsingOrgs := []string{clientOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil { if err != nil {
return fmt.Errorf("failed setting state based endorsement for owner: %v", err) 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 // Persist private immutable asset properties to owner's private data collection
collection := buildCollectionName(clientOrgID) collection := buildCollectionName(clientOrgID)
err = ctx.GetStub().PutPrivateData(collection, asset.ID, immutablePropertiesJSON) err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
if err != nil { if err != nil {
return fmt.Errorf("failed to put Asset private details: %v", err) return "", fmt.Errorf("failed to put Asset private details: %v", err)
} }
return nil return assetID, nil
} }
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description // 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 { func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
// No need to check client org id matches peer org id, rely on the asset ownership check instead. // No need to check client org id matches peer org id, rely on the asset ownership check instead.
clientOrgID, err := getClientOrgID(ctx, false) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err) return fmt.Errorf("failed to get verified OrgID: %v", err)
} }
@ -121,14 +128,15 @@ func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionConte
return ctx.GetStub().PutState(assetID, updatedAssetJSON) return ctx.GetStub().PutState(assetID, updatedAssetJSON)
} }
// AgreeToSell adds seller's asking price to seller's implicit private data collection // AgreeToSell adds seller's asking price to seller's implicit private data collection and requires to specify the next possible buyer
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error { // Set the endorsement policy such that seller org and passed target buyer org peers are both required to endorse the tranfer
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
asset, err := s.ReadAsset(ctx, assetID) asset, err := s.ReadAsset(ctx, assetID)
if err != nil { if err != nil {
return err return err
} }
clientOrgID, err := getClientOrgID(ctx, true) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err) return fmt.Errorf("failed to get verified OrgID: %v", err)
} }
@ -138,18 +146,48 @@ func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg) return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
} }
// Set the endorsement policy such that owner org and seller org peers are both required to endorse the tranfer
endorsingOrgs := []string{clientOrgID, buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil {
return fmt.Errorf("failed setting state based endorsement for buyer and future seller: %v", err)
}
return agreeToPrice(ctx, assetID, typeAssetForSale) return agreeToPrice(ctx, assetID, typeAssetForSale)
} }
// AgreeToBuy adds buyer's bid price to buyer's implicit private data collection // 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 { 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 fmt.Errorf("failed to get verified OrgID: %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")
}
// 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) return agreeToPrice(ctx, assetID, typeAssetBid)
} }
// agreeToPrice adds a bid or ask price to caller's implicit private data collection // agreeToPrice adds a bid or ask price to caller's implicit private data collection
func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error { func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error {
// In this scenario, client is only authorized to read/write private data from its own peer. // In this scenario, client is only authorized to read/write private data from its own peer.
clientOrgID, err := getClientOrgID(ctx, true) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err) return fmt.Errorf("failed to get verified OrgID: %v", err)
} }
@ -186,6 +224,7 @@ func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, p
// VerifyAssetProperties Allows a buyer to validate the properties of // VerifyAssetProperties Allows a buyer to validate the properties of
// an asset against the owner's implicit private data collection // an asset 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) { func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
transMap, err := ctx.GetStub().GetTransient() transMap, err := ctx.GetStub().GetTransient()
if err != nil { if err != nil {
@ -225,13 +264,22 @@ func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContext
) )
} }
// 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 return true, nil
} }
// TransferAsset checks transfer conditions and then transfers asset state to buyer. // TransferAsset checks transfer conditions and then transfers asset state to buyer.
// TransferAsset can only be called by current owner // TransferAsset can only be called by current owner
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error { func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
clientOrgID, err := getClientOrgID(ctx, false) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get verified OrgID: %v", err) return fmt.Errorf("failed to get verified OrgID: %v", err)
} }
@ -241,11 +289,6 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
return fmt.Errorf("error getting transient data: %v", err) return fmt.Errorf("error getting transient data: %v", err)
} }
immutablePropertiesJSON, ok := transMap["asset_properties"]
if !ok {
return fmt.Errorf("asset_properties key not found in the transient map")
}
priceJSON, ok := transMap["asset_price"] priceJSON, ok := transMap["asset_price"]
if !ok { if !ok {
return fmt.Errorf("asset_price key not found in the transient map") return fmt.Errorf("asset_price key not found in the transient map")
@ -262,12 +305,12 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
return fmt.Errorf("failed to get asset: %v", err) return fmt.Errorf("failed to get asset: %v", err)
} }
err = verifyTransferConditions(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, priceJSON) err = verifyTransferConditions(ctx, asset, clientOrgID, buyerOrgID, priceJSON)
if err != nil { if err != nil {
return fmt.Errorf("failed transfer verification: %v", err) return fmt.Errorf("failed transfer verification: %v", err)
} }
err = transferAssetState(ctx, asset, immutablePropertiesJSON, clientOrgID, buyerOrgID, agreement.Price) err = transferAssetState(ctx, asset, clientOrgID, buyerOrgID, agreement.Price)
if err != nil { if err != nil {
return fmt.Errorf("failed asset transfer: %v", err) return fmt.Errorf("failed asset transfer: %v", err)
} }
@ -279,7 +322,6 @@ func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterfac
// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price // verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price
func verifyTransferConditions(ctx contractapi.TransactionContextInterface, func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
asset *Asset, asset *Asset,
immutablePropertiesJSON []byte,
clientOrgID string, clientOrgID string,
buyerOrgID string, buyerOrgID string,
priceJSON []byte) error { priceJSON []byte) error {
@ -290,27 +332,30 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg) return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg)
} }
// CHECK2: Verify that the hash of the passed immutable properties matches the on-chain hash // CHECK2: Verify that both buyers and seller on-chain asset defintion hash matches
collectionSeller := buildCollectionName(clientOrgID) collectionSeller := buildCollectionName(clientOrgID)
immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID) collectionBuyer := buildCollectionName(buyerOrgID)
sellerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err) return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
} }
if immutablePropertiesOnChainHash == nil { 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) return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
} }
hash := sha256.New()
hash.Write(immutablePropertiesJSON)
calculatedPropertiesHash := hash.Sum(nil)
// verify that the hash of the passed immutable properties matches the on-chain hash // verify that the hash of the passed immutable properties matches the on-chain hash
if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) { if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
return fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x", return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
calculatedPropertiesHash, sellerPropertiesOnChainHash,
immutablePropertiesJSON, buyerPropertiesOnChainHash,
immutablePropertiesOnChainHash,
) )
} }
@ -330,7 +375,6 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
} }
// Get buyers bid price // Get buyers bid price
collectionBuyer := buildCollectionName(buyerOrgID)
assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID}) assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
if err != nil { if err != nil {
return fmt.Errorf("failed to create composite key: %v", err) return fmt.Errorf("failed to create composite key: %v", err)
@ -343,7 +387,7 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
return fmt.Errorf("buyer price for %s does not exist", asset.ID) return fmt.Errorf("buyer price for %s does not exist", asset.ID)
} }
hash = sha256.New() hash := sha256.New()
hash.Write(priceJSON) hash.Write(priceJSON)
calculatedPriceHash := hash.Sum(nil) calculatedPriceHash := hash.Sum(nil)
@ -369,7 +413,8 @@ func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
} }
// transferAssetState performs the public and private state updates for the transferred asset // transferAssetState performs the public and private state updates for the transferred asset
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, immutablePropertiesJSON []byte, clientOrgID string, buyerOrgID string, price int) error { // 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 {
asset.OwnerOrg = buyerOrgID asset.OwnerOrg = buyerOrgID
updatedAsset, err := json.Marshal(asset) updatedAsset, err := json.Marshal(asset)
if err != nil { if err != nil {
@ -381,8 +426,9 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
return fmt.Errorf("failed to write asset for buyer: %v", err) return fmt.Errorf("failed to write asset for buyer: %v", err)
} }
// Change the endorsement policy to the new owner // Changes the endorsement policy to the new owner org
err = setAssetStateBasedEndorsement(ctx, asset.ID, buyerOrgID) endorsingOrgs := []string{buyerOrgID}
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
if err != nil { if err != nil {
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err) return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
} }
@ -395,10 +441,6 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
} }
collectionBuyer := buildCollectionName(buyerOrgID) collectionBuyer := buildCollectionName(buyerOrgID)
err = ctx.GetStub().PutPrivateData(collectionBuyer, asset.ID, immutablePropertiesJSON)
if err != nil {
return fmt.Errorf("failed to put Asset private properties for buyer: %v", err)
}
// Delete the price records for seller // Delete the price records for seller
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID}) assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
@ -466,23 +508,12 @@ func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asse
} }
// getClientOrgID gets the client org ID. // getClientOrgID gets the client org ID.
// The client org ID can optionally be verified against the peer org ID, to ensure that a client func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) {
// from another org doesn't attempt to read or write private data from this peer.
// The only exception in this scenario is for TransferAsset, since the current owner
// needs to get an endorsement from the buyer's peer.
func getClientOrgID(ctx contractapi.TransactionContextInterface, verifyOrg bool) (string, error) {
clientOrgID, err := ctx.GetClientIdentity().GetMSPID() clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
if err != nil { if err != nil {
return "", fmt.Errorf("failed getting client's orgID: %v", err) return "", fmt.Errorf("failed getting client's orgID: %v", err)
} }
if verifyOrg {
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
if err != nil {
return "", err
}
}
return clientOrgID, nil return clientOrgID, nil
} }
@ -503,14 +534,13 @@ func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
return nil return nil
} }
// setAssetStateBasedEndorsement adds an endorsement policy to a asset so that only a peer from an owning org // setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
// can update or transfer the asset. func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgToEndorse string) error {
endorsementPolicy, err := statebased.NewStateEP(nil) endorsementPolicy, err := statebased.NewStateEP(nil)
if err != nil { if err != nil {
return err return err
} }
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgToEndorse) err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgsToEndorse...)
if err != nil { if err != nil {
return fmt.Errorf("failed to add org to endorsement policy: %v", err) return fmt.Errorf("failed to add org to endorsement policy: %v", err)
} }
@ -526,12 +556,40 @@ func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface,
return nil return nil
} }
// GetAssetHash Allows a buyer to validate the properties of
// an asset against the asset Id and return 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 buildCollectionName(clientOrgID string) string { func buildCollectionName(clientOrgID string) string {
return fmt.Sprintf("_implicit_org_%s", clientOrgID) return fmt.Sprintf("_implicit_org_%s", clientOrgID)
} }
func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) { func getClientImplicitCollectionName(ctx contractapi.TransactionContextInterface) (string, error) {
clientOrgID, err := getClientOrgID(ctx, true) clientOrgID, err := getClientOrgID(ctx)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get verified OrgID: %v", err) return "", fmt.Errorf("failed to get verified OrgID: %v", err)
} }