mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
Fix private-data TypeScript chaincode (#1357)
The build is only testing the Go chaincode for the asset-transfer-private-data sample. The behavior of the TypeScript chaincode implementation is not consistent with the Go and Java versions, which prevents it from working correctly. This change fixes the TypeScript chaincode implementation for the asset-transfer-private-data sample so that it works correctly. Additionally, some JSON property names are explicitly set in the Go client application sample, which prevented it from working with the Java and TypeScript chaincode implementations. Signed-off-by: Mark S. Lewis <Mark.S.Lewis@outlook.com>
This commit is contained in:
parent
a72b2b5132
commit
f865a9ea51
7 changed files with 865 additions and 417 deletions
6
.github/workflows/test-network-private.yaml
vendored
6
.github/workflows/test-network-private.yaml
vendored
|
|
@ -7,9 +7,9 @@ run-name: ${{ github.actor }} is running the Test Network Private tests 🔒
|
|||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main", "release-2.5" ]
|
||||
branches: ["main", "release-2.5"]
|
||||
pull_request:
|
||||
branches: [ "main", "release-2.5" ]
|
||||
branches: ["main", "release-2.5"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
|
@ -22,6 +22,8 @@ jobs:
|
|||
matrix:
|
||||
chaincode-language:
|
||||
- go
|
||||
- java
|
||||
- typescript
|
||||
chaincode-name:
|
||||
- private
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -162,11 +162,11 @@ func createAssets(contract *client.Contract) {
|
|||
fmt.Printf("\n--> Submit Transaction: CreateAsset, ID: %s\n", assetID1)
|
||||
|
||||
type assetTransientInput struct {
|
||||
ObjectType string
|
||||
AssetID string
|
||||
Color string
|
||||
Size uint8
|
||||
AppraisedValue uint16
|
||||
ObjectType string `json:"objectType"`
|
||||
AssetID string `json:"assetID"`
|
||||
Color string `json:"color"`
|
||||
Size uint8 `json:"size"`
|
||||
AppraisedValue uint16 `json:"appraisedValue"`
|
||||
}
|
||||
|
||||
asset1Data := assetTransientInput{
|
||||
|
|
@ -300,7 +300,9 @@ func transferAsset(contract *client.Contract, assetID string) (err error) {
|
|||
func deleteAsset(contract *client.Contract, assetID string) (err error) {
|
||||
fmt.Printf("\n--> Submit Transaction: DeleteAsset, ID: %s\n", assetID)
|
||||
|
||||
dataForDelete := struct{ AssetID string }{assetID}
|
||||
dataForDelete := struct {
|
||||
AssetID string `json:"assetID"`
|
||||
}{assetID}
|
||||
|
||||
if _, err = contract.Submit(
|
||||
"DeleteAsset",
|
||||
|
|
@ -318,7 +320,9 @@ func deleteAsset(contract *client.Contract, assetID string) (err error) {
|
|||
func purgeAsset(contract *client.Contract, assetID string) (err error) {
|
||||
fmt.Printf("\n--> Submit Transaction: PurgeAsset, ID: %s\n", assetID)
|
||||
|
||||
dataForPurge := struct{ AssetID string }{assetID}
|
||||
dataForPurge := struct {
|
||||
AssetID string `json:"assetID"`
|
||||
}{assetID}
|
||||
|
||||
if _, err = contract.Submit(
|
||||
"PurgeAsset",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -27,8 +27,8 @@
|
|||
"dependencies": {
|
||||
"fabric-contract-api": "~2.5",
|
||||
"fabric-shim": "~2.5",
|
||||
"json-stringify-deterministic": "^1.0.0",
|
||||
"sort-keys-recursive": "^2.1.0"
|
||||
"json-stringify-deterministic": "^1.0.12",
|
||||
"sort-keys-recursive": "^2.1.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.19.33",
|
||||
|
|
|
|||
|
|
@ -2,41 +2,52 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Object, Property } from "fabric-contract-api";
|
||||
import { nonEmptyString, positiveNumber } from "./utils";
|
||||
import { Object, Property } from 'fabric-contract-api';
|
||||
import { nonEmptyString, positiveNumber } from './utils';
|
||||
|
||||
@Object()
|
||||
// Asset describes main asset details that are visible to all organizations
|
||||
export class Asset {
|
||||
@Property()
|
||||
docType?: string;
|
||||
objectType?: string;
|
||||
|
||||
@Property()
|
||||
ID: string = "";
|
||||
ID: string = '';
|
||||
|
||||
@Property()
|
||||
Color: string = "";
|
||||
Color: string = '';
|
||||
|
||||
@Property()
|
||||
Size: number = 0;
|
||||
|
||||
@Property()
|
||||
Owner: string = "";
|
||||
Owner: string = '';
|
||||
|
||||
static fromBytes(bytes: Uint8Array): Asset {
|
||||
if (bytes.length === 0) {
|
||||
throw new Error("no asset data");
|
||||
throw new Error('no asset data');
|
||||
}
|
||||
const json = Buffer.from(bytes).toString();
|
||||
const json = Buffer.from(bytes).toString('utf8');
|
||||
const properties = JSON.parse(json) as Partial<Asset>;
|
||||
|
||||
const result = new Asset();
|
||||
result.docType = properties.docType;
|
||||
result.ID = nonEmptyString(properties.ID, "ID field must be a non-empty string");
|
||||
result.Color = nonEmptyString(properties.Color, "Color field must be a non-empty string");
|
||||
result.Size = positiveNumber(properties.Size, "Size field must be a positive integer");
|
||||
result.Owner = nonEmptyString(properties.Owner, "appraiseOwner field must be a non-empty string");
|
||||
|
||||
return result;
|
||||
return {
|
||||
objectType: properties.objectType,
|
||||
ID: nonEmptyString(
|
||||
properties.ID,
|
||||
'ID field must be a non-empty string'
|
||||
),
|
||||
Color: nonEmptyString(
|
||||
properties.Color,
|
||||
'Color field must be a non-empty string'
|
||||
),
|
||||
Size: positiveNumber(
|
||||
properties.Size,
|
||||
'Size field must be a positive integer'
|
||||
),
|
||||
Owner: nonEmptyString(
|
||||
properties.Owner,
|
||||
'appraiseOwner field must be a non-empty string'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,36 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { Context, Contract, Info, Transaction } from 'fabric-contract-api';
|
||||
import {
|
||||
Context,
|
||||
Contract,
|
||||
Info,
|
||||
Returns,
|
||||
Transaction,
|
||||
} from 'fabric-contract-api';
|
||||
import stringify from 'json-stringify-deterministic';
|
||||
import sortKeysRecursive from 'sort-keys-recursive';
|
||||
import { Asset } from './asset';
|
||||
import { AssetPrivateDetails } from './assetTransferDetails';
|
||||
import { TransientAssetDelete, TransientAssetOwner, TransientAssetProperties, TransientAssetPurge, TransientAssetValue } from './assetTransferTransientInput';
|
||||
import {
|
||||
readTransientAssetOwner,
|
||||
readTransientAssetValue,
|
||||
TransientAssetDelete,
|
||||
TransientAssetProperties,
|
||||
TransientAssetPurge,
|
||||
} from './assetTransferTransientInput';
|
||||
import { TransferAgreement } from './transferAgreement';
|
||||
|
||||
const assetCollection = 'assetCollection';
|
||||
const transferAgreementObjectType = 'transferAgreement';
|
||||
|
||||
@Info({ title: 'AssetTransfer', description: 'Smart contract for trading assets' })
|
||||
export class AssetTransfer extends Contract {
|
||||
const utf8Encoder = new TextEncoder();
|
||||
|
||||
@Info({
|
||||
title: 'AssetTransfer',
|
||||
description: 'Smart contract for trading assets',
|
||||
})
|
||||
export class AssetTransfer extends Contract {
|
||||
// CreateAsset issues a new asset to the world state with given details.
|
||||
@Transaction()
|
||||
public async CreateAsset(ctx: Context): Promise<void> {
|
||||
|
|
@ -22,9 +38,14 @@ export class AssetTransfer extends Contract {
|
|||
const assetProperties = new TransientAssetProperties(transientMap);
|
||||
|
||||
// Check if asset already exists
|
||||
const assetAsBytes = await ctx.stub.getPrivateData(assetCollection, assetProperties.assetID);
|
||||
const assetAsBytes = await ctx.stub.getPrivateData(
|
||||
assetCollection,
|
||||
assetProperties.assetID
|
||||
);
|
||||
if (assetAsBytes.length !== 0) {
|
||||
throw new Error('this asset already exists: ' + assetProperties.assetID);
|
||||
throw new Error(
|
||||
'this asset already exists: ' + assetProperties.assetID
|
||||
);
|
||||
}
|
||||
|
||||
// Get ID of submitting client identity
|
||||
|
|
@ -45,7 +66,11 @@ export class AssetTransfer extends Contract {
|
|||
// Save asset to private data collection
|
||||
// Typical logger, logs to stdout/file in the fabric managed docker container, running this chaincode
|
||||
// Look for container name like dev-peer0.org1.example.com-{chaincodename_version}-xyz
|
||||
await ctx.stub.putPrivateData(assetCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
await ctx.stub.putPrivateData(
|
||||
assetCollection,
|
||||
asset.ID,
|
||||
marshal(asset)
|
||||
);
|
||||
|
||||
// Save asset details to collection visible to owning organization
|
||||
const assetPrivateDetails: AssetPrivateDetails = {
|
||||
|
|
@ -55,8 +80,16 @@ export class AssetTransfer extends Contract {
|
|||
// Get collection name for this organization.
|
||||
const orgCollection = this.getCollectionName(ctx);
|
||||
// Put asset appraised value into owners org specific private data collection
|
||||
console.log('Put: collection %v, ID %v', orgCollection, assetProperties.assetID);
|
||||
await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(assetPrivateDetails))));
|
||||
console.log(
|
||||
'Put: collection %v, ID %v',
|
||||
orgCollection,
|
||||
assetProperties.assetID
|
||||
);
|
||||
await ctx.stub.putPrivateData(
|
||||
orgCollection,
|
||||
asset.ID,
|
||||
marshal(assetPrivateDetails)
|
||||
);
|
||||
}
|
||||
|
||||
// AgreeToTransfer is used by the potential buyer of the asset to agree to the
|
||||
|
|
@ -69,7 +102,7 @@ export class AssetTransfer extends Contract {
|
|||
const clientID = ctx.clientIdentity.getID();
|
||||
// Value is private, therefore it gets passed in transient field
|
||||
const transientMap = ctx.stub.getTransient();
|
||||
const assetValue = new TransientAssetValue(transientMap);
|
||||
const assetValue = readTransientAssetValue(transientMap);
|
||||
|
||||
const valueJSON: AssetPrivateDetails = {
|
||||
ID: assetValue.assetID,
|
||||
|
|
@ -77,20 +110,38 @@ export class AssetTransfer extends Contract {
|
|||
};
|
||||
// Read asset from the private data collection
|
||||
const asset = await this.ReadAsset(ctx, valueJSON.ID);
|
||||
if (!asset) {
|
||||
throw new Error(`${valueJSON.ID} does not exist`);
|
||||
}
|
||||
// Verify that the client is submitting request to peer in their organization
|
||||
this.verifyClientOrgMatchesPeerOrg(ctx);
|
||||
|
||||
// Get collection name for this organization. Needs to be read by a member of the organization.
|
||||
const orgCollection = this.getCollectionName(ctx);
|
||||
console.log(`AgreeToTransfer Put: collection ${orgCollection}, ID ${valueJSON.ID}`);
|
||||
console.log(
|
||||
`AgreeToTransfer Put: collection ${orgCollection}, ID ${valueJSON.ID}`
|
||||
);
|
||||
// Put agreed value in the org specifc private data collection
|
||||
await ctx.stub.putPrivateData(orgCollection, asset.ID, Buffer.from(stringify(sortKeysRecursive(valueJSON))));
|
||||
await ctx.stub.putPrivateData(
|
||||
orgCollection,
|
||||
asset.ID,
|
||||
marshal(valueJSON)
|
||||
);
|
||||
// Create agreeement that indicates which identity has agreed to purchase
|
||||
// In a more realistic transfer scenario, a transfer agreement would be secured to ensure that it cannot
|
||||
// be overwritten by another channel member
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [valueJSON.ID]);
|
||||
console.log(`AgreeToTransfer Put: collection ${assetCollection}, ID ${valueJSON.ID}, Key ${transferAgreeKey}`);
|
||||
await ctx.stub.putPrivateData(assetCollection, transferAgreeKey, new Uint8Array(Buffer.from(clientID)));
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(
|
||||
transferAgreementObjectType,
|
||||
[valueJSON.ID]
|
||||
);
|
||||
console.log(
|
||||
`AgreeToTransfer Put: collection ${assetCollection}, ID ${valueJSON.ID}, Key ${transferAgreeKey}`
|
||||
);
|
||||
await ctx.stub.putPrivateData(
|
||||
assetCollection,
|
||||
transferAgreeKey,
|
||||
utf8Encoder.encode(clientID)
|
||||
);
|
||||
}
|
||||
|
||||
@Transaction()
|
||||
|
|
@ -98,31 +149,57 @@ export class AssetTransfer extends Contract {
|
|||
public async TransferAsset(ctx: Context): Promise<void> {
|
||||
// Asset properties are private, therefore they get passed in transient field
|
||||
const transientMap = ctx.stub.getTransient();
|
||||
const assetOwner = new TransientAssetOwner(transientMap);
|
||||
const assetOwner = readTransientAssetOwner(transientMap);
|
||||
|
||||
console.log('TransferAsset: verify asset exists ID ' + assetOwner.assetID);
|
||||
console.log(
|
||||
'TransferAsset: verify asset exists ID ' + assetOwner.assetID
|
||||
);
|
||||
// Read asset from the private data collection
|
||||
const asset = await this.ReadAsset(ctx, assetOwner.assetID);
|
||||
if (!asset) {
|
||||
throw new Error(`${assetOwner.assetID} does not exist`);
|
||||
}
|
||||
// Verify that the client is submitting request to peer in their organization
|
||||
this.verifyClientOrgMatchesPeerOrg(ctx);
|
||||
// Verify transfer details and transfer owner
|
||||
await this.verifyAgreement(ctx, assetOwner.assetID, asset.Owner, assetOwner.buyerMSP);
|
||||
await this.verifyAgreement(
|
||||
ctx,
|
||||
assetOwner.assetID,
|
||||
asset.Owner,
|
||||
assetOwner.buyerMSP
|
||||
);
|
||||
|
||||
const transferAgreement = await this.ReadTransferAgreement(
|
||||
ctx,
|
||||
assetOwner.assetID
|
||||
);
|
||||
|
||||
const transferAgreement = await this.ReadTransferAgreement(ctx, assetOwner.assetID);
|
||||
if (transferAgreement.BuyerID === '') {
|
||||
throw new Error('BuyerID not found in TransferAgreement for ' + assetOwner.assetID);
|
||||
throw new Error(
|
||||
'BuyerID not found in TransferAgreement for ' +
|
||||
assetOwner.assetID
|
||||
);
|
||||
}
|
||||
// Transfer asset in private data collection to new owner
|
||||
asset.Owner = transferAgreement.BuyerID;
|
||||
console.log(`TransferAsset Put: collection ${assetCollection}, ID ${assetOwner.assetID}`);
|
||||
await ctx.stub.putPrivateData(assetCollection, assetOwner.assetID, Buffer.from(stringify(sortKeysRecursive(asset)))); // rewrite the asset
|
||||
console.log(
|
||||
`TransferAsset Put: collection ${assetCollection}, ID ${assetOwner.assetID}`
|
||||
);
|
||||
await ctx.stub.putPrivateData(
|
||||
assetCollection,
|
||||
assetOwner.assetID,
|
||||
marshal(asset)
|
||||
); // rewrite the asset
|
||||
|
||||
// Get collection name for this organization
|
||||
const ownersCollection = this.getCollectionName(ctx);
|
||||
// Delete the asset appraised value from this organization's private data collection
|
||||
await ctx.stub.deletePrivateData(ownersCollection, assetOwner.assetID);
|
||||
// Delete the transfer agreement from the asset collection
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [assetOwner.assetID]);
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(
|
||||
transferAgreementObjectType,
|
||||
[assetOwner.assetID]
|
||||
);
|
||||
await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey);
|
||||
}
|
||||
|
||||
|
|
@ -138,15 +215,23 @@ export class AssetTransfer extends Contract {
|
|||
|
||||
console.log('Deleting Asset: ' + assetDelete.assetID);
|
||||
// get the asset from chaincode state
|
||||
const valAsbytes = await ctx.stub.getPrivateData(assetCollection, assetDelete.assetID);
|
||||
const valAsbytes = await ctx.stub.getPrivateData(
|
||||
assetCollection,
|
||||
assetDelete.assetID
|
||||
);
|
||||
if (valAsbytes.length === 0) {
|
||||
throw new Error('asset not found: ' + assetDelete.assetID);
|
||||
}
|
||||
const ownerCollection = this.getCollectionName(ctx);
|
||||
// Check the asset is in the caller org's private collection
|
||||
const valAsbytesPrivate = await ctx.stub.getPrivateData(ownerCollection, assetDelete.assetID);
|
||||
const valAsbytesPrivate = await ctx.stub.getPrivateData(
|
||||
ownerCollection,
|
||||
assetDelete.assetID
|
||||
);
|
||||
if (valAsbytesPrivate.length === 0) {
|
||||
throw new Error(`asset not found in owner's private Collection: ${ownerCollection} : ${assetDelete.assetID}`);
|
||||
throw new Error(
|
||||
`asset not found in owner's private Collection: ${ownerCollection} : ${assetDelete.assetID}`
|
||||
);
|
||||
}
|
||||
// delete the asset from state
|
||||
await ctx.stub.deletePrivateData(assetCollection, assetDelete.assetID);
|
||||
|
|
@ -191,15 +276,26 @@ export class AssetTransfer extends Contract {
|
|||
// Get proposers collection.
|
||||
const orgCollection = this.getCollectionName(ctx);
|
||||
// Delete the transfer agreement from the asset collection
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [agreementDelete.assetID]);
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(
|
||||
transferAgreementObjectType,
|
||||
[agreementDelete.assetID]
|
||||
);
|
||||
// get the transfer_agreement
|
||||
const valAsbytes = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey);
|
||||
const valAsbytes = await ctx.stub.getPrivateData(
|
||||
assetCollection,
|
||||
transferAgreeKey
|
||||
);
|
||||
|
||||
if (valAsbytes.length === 0) {
|
||||
throw new Error(`asset's transfer_agreement does not exist: ${agreementDelete.assetID}`);
|
||||
throw new Error(
|
||||
`asset's transfer_agreement does not exist: ${agreementDelete.assetID}`
|
||||
);
|
||||
}
|
||||
// Delete the asset
|
||||
await ctx.stub.deletePrivateData(orgCollection, agreementDelete.assetID);
|
||||
await ctx.stub.deletePrivateData(
|
||||
orgCollection,
|
||||
agreementDelete.assetID
|
||||
);
|
||||
// Delete transfer agreement record, remove agreement from state
|
||||
await ctx.stub.deletePrivateData(assetCollection, transferAgreeKey);
|
||||
}
|
||||
|
|
@ -209,36 +305,59 @@ export class AssetTransfer extends Contract {
|
|||
*/
|
||||
|
||||
// ReadAsset reads the information from collection
|
||||
@Transaction()
|
||||
public async ReadAsset(ctx: Context, id: string): Promise<Asset> {
|
||||
@Transaction(false)
|
||||
@Returns('Asset')
|
||||
public async ReadAsset(
|
||||
ctx: Context,
|
||||
id: string
|
||||
): Promise<Asset | undefined> {
|
||||
// Check if asset already exists
|
||||
const assetAsBytes = await ctx.stub.getPrivateData(assetCollection, id);
|
||||
// No Asset found, return empty response
|
||||
if (assetAsBytes.length === 0) {
|
||||
throw new Error(id + ' does not exist in collection ' + assetCollection);
|
||||
console.log(
|
||||
id + ' does not exist in collection ' + assetCollection
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return Asset.fromBytes(assetAsBytes);
|
||||
}
|
||||
|
||||
// ReadAssetPrivateDetails reads the asset private details in organization specific collection
|
||||
@Transaction()
|
||||
public async ReadAssetPrivateDetails(ctx: Context, collection: string, id: string): Promise<AssetPrivateDetails> {
|
||||
@Transaction(false)
|
||||
@Returns('AssetPrivateDetails')
|
||||
public async ReadAssetPrivateDetails(
|
||||
ctx: Context,
|
||||
collection: string,
|
||||
id: string
|
||||
): Promise<AssetPrivateDetails | undefined> {
|
||||
// Check if asset already exists
|
||||
const detailBytes = await ctx.stub.getPrivateData(collection, id);
|
||||
// No Asset found, return empty response
|
||||
if (detailBytes.length === 0) {
|
||||
throw new Error(id + ' does not exist in collection ' + collection);
|
||||
console.log(id + ' does not exist in collection ' + collection);
|
||||
return undefined;
|
||||
}
|
||||
return AssetPrivateDetails.fromBytes(detailBytes);
|
||||
}
|
||||
|
||||
// ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection
|
||||
@Transaction()
|
||||
public async ReadTransferAgreement(ctx: Context, assetID: string): Promise<TransferAgreement> {
|
||||
@Transaction(false)
|
||||
@Returns('TransferAgreement')
|
||||
public async ReadTransferAgreement(
|
||||
ctx: Context,
|
||||
assetID: string
|
||||
): Promise<TransferAgreement> {
|
||||
// composite key for TransferAgreement of this asset
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(transferAgreementObjectType, [assetID]);
|
||||
const transferAgreeKey = ctx.stub.createCompositeKey(
|
||||
transferAgreementObjectType,
|
||||
[assetID]
|
||||
);
|
||||
// Get the identity from collection
|
||||
const buyerIdentity = await ctx.stub.getPrivateData(assetCollection, transferAgreeKey);
|
||||
const buyerIdentity = await ctx.stub.getPrivateData(
|
||||
assetCollection,
|
||||
transferAgreeKey
|
||||
);
|
||||
|
||||
if (buyerIdentity.length === 0) {
|
||||
throw new Error(`TransferAgreement for ${assetID} does not exist `);
|
||||
|
|
@ -253,9 +372,18 @@ export class AssetTransfer extends Contract {
|
|||
// GetAssetByRange performs a range query based on the start and end keys provided. Range
|
||||
// queries can be used to read data from private data collections, but can not be used in
|
||||
// a transaction that also writes to private data.
|
||||
@Transaction()
|
||||
public async GetAssetByRange(ctx: Context, startKey: string, endKey: string): Promise<Asset[]> {
|
||||
const resultsIterator = ctx.stub.getPrivateDataByRange(assetCollection, startKey, endKey);
|
||||
@Transaction(false)
|
||||
@Returns('Asset[]')
|
||||
public async GetAssetByRange(
|
||||
ctx: Context,
|
||||
startKey: string,
|
||||
endKey: string
|
||||
): Promise<Asset[]> {
|
||||
const resultsIterator = ctx.stub.getPrivateDataByRange(
|
||||
assetCollection,
|
||||
startKey,
|
||||
endKey
|
||||
);
|
||||
const results: Asset[] = [];
|
||||
for await (const res of resultsIterator) {
|
||||
const asset = Asset.fromBytes(res.value);
|
||||
|
|
@ -269,12 +397,19 @@ export class AssetTransfer extends Contract {
|
|||
// verifyAgreement is an internal helper function used by TransferAsset to verify
|
||||
// that the transfer is being initiated by the owner and that the buyer has agreed
|
||||
// to the same appraisal value as the owner
|
||||
public async verifyAgreement(ctx: Context, assetID: string, owner: string, buyerMSP: string): Promise<void> {
|
||||
public async verifyAgreement(
|
||||
ctx: Context,
|
||||
assetID: string,
|
||||
owner: string,
|
||||
buyerMSP: string
|
||||
): Promise<void> {
|
||||
// Check 1: verify that the transfer is being initiatied by the owner
|
||||
// Get ID of submitting client identity
|
||||
const clientID = ctx.clientIdentity.getID();
|
||||
if (clientID !== owner) {
|
||||
throw new Error(`error: submitting client(${clientID}) identity does not own asset ${assetID}.Owner is ${owner}`);
|
||||
throw new Error(
|
||||
`error: submitting client(${clientID}) identity does not own asset ${assetID}.Owner is ${owner}`
|
||||
);
|
||||
}
|
||||
// Check 2: verify that the buyer has agreed to the appraised value
|
||||
// Get collection names
|
||||
|
|
@ -282,19 +417,38 @@ export class AssetTransfer extends Contract {
|
|||
|
||||
const collectionBuyer = buyerMSP + 'PrivateCollection'; // get buyers collection
|
||||
// Get hash of owners agreed to value
|
||||
const ownerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionOwner, assetID);
|
||||
const ownerAppraisedValueHash = await ctx.stub.getPrivateDataHash(
|
||||
collectionOwner,
|
||||
assetID
|
||||
);
|
||||
|
||||
if (ownerAppraisedValueHash.length === 0) {
|
||||
throw new Error(`hash of appraised value for ${assetID} does not exist in collection ${collectionOwner}`);
|
||||
throw new Error(
|
||||
`hash of appraised value for ${assetID} does not exist in collection ${collectionOwner}`
|
||||
);
|
||||
}
|
||||
// Get hash of buyers agreed to value
|
||||
const buyerAppraisedValueHash = await ctx.stub.getPrivateDataHash(collectionBuyer, assetID);
|
||||
const buyerAppraisedValueHash = await ctx.stub.getPrivateDataHash(
|
||||
collectionBuyer,
|
||||
assetID
|
||||
);
|
||||
if (buyerAppraisedValueHash.length === 0) {
|
||||
throw new Error(`hash of appraised value for ${assetID} does not exist in collection ${collectionBuyer}. AgreeToTransfer must be called by the buyer first`);
|
||||
throw new Error(
|
||||
`hash of appraised value for ${assetID} does not exist in collection ${collectionBuyer}. AgreeToTransfer must be called by the buyer first`
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the two hashes match
|
||||
if (ownerAppraisedValueHash.toString() !== buyerAppraisedValueHash.toString()) {
|
||||
throw new Error(`hash for appraised value for owner ${Buffer.from(ownerAppraisedValueHash).toString('hex')} does not match value for seller ${Buffer.from(buyerAppraisedValueHash).toString('hex')}`);
|
||||
const ownerValueHashHex = Buffer.from(ownerAppraisedValueHash).toString(
|
||||
'hex'
|
||||
);
|
||||
const buyerValueHashHex = Buffer.from(buyerAppraisedValueHash).toString(
|
||||
'hex'
|
||||
);
|
||||
if (ownerValueHashHex !== buyerValueHashHex) {
|
||||
throw new Error(
|
||||
`hash for appraised value for owner ${ownerValueHashHex} does not match value for buyer ${buyerValueHashHex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// getCollectionName is an internal helper function to get collection of submitting client identity.
|
||||
|
|
@ -308,7 +462,6 @@ export class AssetTransfer extends Contract {
|
|||
}
|
||||
// Get ID of submitting client identity
|
||||
public submittingClientIdentity(ctx: Context): string {
|
||||
|
||||
const b64ID = ctx.clientIdentity.getID();
|
||||
|
||||
// base64.StdEncoding.DecodeString(b64ID);
|
||||
|
|
@ -318,13 +471,17 @@ export class AssetTransfer extends Contract {
|
|||
}
|
||||
// verifyClientOrgMatchesPeerOrg is an internal function used verify client org id and matches peer org id.
|
||||
public verifyClientOrgMatchesPeerOrg(ctx: Context): void {
|
||||
|
||||
const clientMSPID = ctx.clientIdentity.getMSPID();
|
||||
|
||||
const peerMSPID = ctx.stub.getMspID();
|
||||
|
||||
if (clientMSPID !== peerMSPID) {
|
||||
throw new Error('client from org %v is not authorized to read or write private data from an org ' + clientMSPID + ' peer ' + peerMSPID);
|
||||
throw new Error(
|
||||
'client from org %v is not authorized to read or write private data from an org ' +
|
||||
clientMSPID +
|
||||
' peer ' +
|
||||
peerMSPID
|
||||
);
|
||||
}
|
||||
}
|
||||
// =======Rich queries =========================================================================
|
||||
|
|
@ -347,20 +504,28 @@ export class AssetTransfer extends Contract {
|
|||
// and accepting a single query parameter (owner).
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// =========================================================================================
|
||||
public async QueryAssetByOwner(ctx: Context, assetType: string, owner: string): Promise<Asset[]> {
|
||||
|
||||
public async QueryAssetByOwner(
|
||||
ctx: Context,
|
||||
assetType: string,
|
||||
owner: string
|
||||
): Promise<Asset[]> {
|
||||
const queryString = `{'selector':{'objectType':'${assetType}','owner':'${owner}'}}`;
|
||||
|
||||
return await this.getQueryResultForQueryString(ctx, queryString);
|
||||
}
|
||||
|
||||
public QueryAssets(ctx: Context, queryString: string): Promise<Asset[]> {
|
||||
public QueryAssets(ctx: Context, queryString: string): Promise<Asset[]> {
|
||||
return this.getQueryResultForQueryString(ctx, queryString);
|
||||
}
|
||||
|
||||
public async getQueryResultForQueryString(ctx: Context, queryString: string): Promise<Asset[]> {
|
||||
|
||||
const resultsIterator = ctx.stub.getPrivateDataQueryResult(assetCollection, queryString);
|
||||
public async getQueryResultForQueryString(
|
||||
ctx: Context,
|
||||
queryString: string
|
||||
): Promise<Asset[]> {
|
||||
const resultsIterator = ctx.stub.getPrivateDataQueryResult(
|
||||
assetCollection,
|
||||
queryString
|
||||
);
|
||||
|
||||
const results: Asset[] = [];
|
||||
|
||||
|
|
@ -372,3 +537,11 @@ export class AssetTransfer extends Contract {
|
|||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
function marshal(o: object): Uint8Array {
|
||||
return utf8Encoder.encode(toJSON(o));
|
||||
}
|
||||
|
||||
function toJSON(o: object): string {
|
||||
return stringify(sortKeysRecursive(o));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { nonEmptyString, positiveNumber } from "./utils";
|
||||
import { nonEmptyString, positiveNumber } from './utils';
|
||||
|
||||
export class TransientAssetProperties {
|
||||
objectType: string;
|
||||
|
|
@ -12,73 +12,105 @@ export class TransientAssetProperties {
|
|||
appraisedValue: number;
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("asset_properties");
|
||||
const transient = transientMap.get('asset_properties');
|
||||
if (!transient?.length) {
|
||||
throw new Error("no asset properties");
|
||||
throw new Error('no asset properties');
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetProperties>;
|
||||
const properties = JSON.parse(
|
||||
json
|
||||
) as Partial<TransientAssetProperties>;
|
||||
|
||||
this.objectType = nonEmptyString(properties.objectType, "objectType field must be a non-empty string");
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.color = nonEmptyString(properties.color, "color field must be a non-empty string");
|
||||
this.size = positiveNumber(properties.size, "size field must be a positive integer");
|
||||
this.objectType = nonEmptyString(
|
||||
properties.objectType,
|
||||
'objectType field must be a non-empty string'
|
||||
);
|
||||
this.assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
this.color = nonEmptyString(
|
||||
properties.color,
|
||||
'color field must be a non-empty string'
|
||||
);
|
||||
this.size = positiveNumber(
|
||||
properties.size,
|
||||
'size field must be a positive integer'
|
||||
);
|
||||
this.appraisedValue = positiveNumber(
|
||||
properties.appraisedValue,
|
||||
"appraisedValue field must be a positive integer"
|
||||
'appraisedValue field must be a positive integer'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransientAssetValue {
|
||||
export interface TransientAssetValue {
|
||||
assetID: string;
|
||||
appraisedValue: number;
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("asset_value");
|
||||
if (!transient?.length) {
|
||||
throw new Error("no asset value");
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetValue>;
|
||||
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.appraisedValue = positiveNumber(
|
||||
properties.appraisedValue,
|
||||
"appraisedValue field must be a positive integer"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransientAssetOwner {
|
||||
export function readTransientAssetValue(
|
||||
transientMap: Map<string, Uint8Array>
|
||||
): TransientAssetValue {
|
||||
const transient = transientMap.get('asset_value');
|
||||
if (!transient?.length) {
|
||||
throw new Error('no asset value');
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetValue>;
|
||||
|
||||
const assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
const appraisedValue = positiveNumber(
|
||||
properties.appraisedValue,
|
||||
'appraisedValue field must be a positive integer'
|
||||
);
|
||||
|
||||
return { assetID, appraisedValue };
|
||||
}
|
||||
|
||||
export interface TransientAssetOwner {
|
||||
assetID: string;
|
||||
buyerMSP: string;
|
||||
}
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("asset_owner");
|
||||
if (!transient?.length) {
|
||||
throw new Error("no asset owner");
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
|
||||
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.buyerMSP = nonEmptyString(properties.buyerMSP, "buyerMSP field must be a non-empty string");
|
||||
export function readTransientAssetOwner(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get('asset_owner');
|
||||
if (!transient?.length) {
|
||||
throw new Error('no asset owner');
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
|
||||
|
||||
const assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
const buyerMSP = nonEmptyString(
|
||||
properties.buyerMSP,
|
||||
'buyerMSP field must be a non-empty string'
|
||||
);
|
||||
|
||||
return { assetID, buyerMSP };
|
||||
}
|
||||
|
||||
export class TransientAssetDelete {
|
||||
assetID: string;
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("asset_delete");
|
||||
const transient = transientMap.get('asset_delete');
|
||||
if (!transient?.length) {
|
||||
throw new Error("no asset delete");
|
||||
throw new Error('no asset delete');
|
||||
}
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
|
||||
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,15 +118,18 @@ export class TransientAssetPurge {
|
|||
assetID: string;
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("asset_purge");
|
||||
const transient = transientMap.get('asset_purge');
|
||||
if (!transient?.length) {
|
||||
throw new Error("no asset purge");
|
||||
throw new Error('no asset purge');
|
||||
}
|
||||
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
|
||||
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,14 +137,17 @@ export class TransientAgreementDelete {
|
|||
assetID: string;
|
||||
|
||||
constructor(transientMap: Map<string, Uint8Array>) {
|
||||
const transient = transientMap.get("agreement_delete");
|
||||
const transient = transientMap.get('agreement_delete');
|
||||
if (!transient?.length) {
|
||||
throw new Error("no agreement delete");
|
||||
throw new Error('no agreement delete');
|
||||
}
|
||||
|
||||
const json = Buffer.from(transient).toString();
|
||||
const properties = JSON.parse(json) as Partial<TransientAssetOwner>;
|
||||
|
||||
this.assetID = nonEmptyString(properties.assetID, "assetID field must be a non-empty string");
|
||||
this.assetID = nonEmptyString(
|
||||
properties.assetID,
|
||||
'assetID field must be a non-empty string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue