/
AssetTransfer.java
593 lines (516 loc) · 28.1 KB
/
AssetTransfer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
/*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.samples.privatedata;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Info;
import org.hyperledger.fabric.contract.annotation.License;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import org.hyperledger.fabric.shim.ledger.CompositeKey;
import org.hyperledger.fabric.shim.ledger.KeyValue;
import org.hyperledger.fabric.shim.ledger.QueryResultsIterator;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Main Chaincode class. A ContractInterface gets converted to Chaincode internally.
* @see org.hyperledger.fabric.shim.Chaincode
*
* Each chaincode transaction function must take, Context as first parameter.
* Unless specified otherwise via annotation (@Contract or @Transaction), the contract name
* is the class name (without package)
* and the transaction name is the method name.
*
* To create fabric test-network
* cd fabric-samples/test-network
* ./network.sh up createChannel -ca -s couchdb
* To deploy this chaincode to test-network, use the collection config as described in
* See <a href="https://hyperledger-fabric.readthedocs.io/en/latest/private_data_tutorial.html</a>
* Change both -ccs sequence & -ccv version args for iterative deployment
* ./network.sh deployCC -ccn private -ccp ../asset-transfer-private-data/chaincode-java/ -ccl java -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cccg ../asset-transfer-private-data/chaincode-go/collections_config.json -ccs 1 -ccv 1
*/
@Contract(
name = "private",
info = @Info(
title = "Asset Transfer Private Data",
description = "The hyperlegendary asset transfer private data",
version = "0.0.1-SNAPSHOT",
license = @License(
name = "Apache 2.0 License",
url = "http://www.apache.org/licenses/LICENSE-2.0.html"),
contact = @Contact(
email = "a.transfer@example.com",
name = "Private Transfer",
url = "https://hyperledger.example.com")))
@Default
public final class AssetTransfer implements ContractInterface {
static final String ASSET_COLLECTION_NAME = "assetCollection";
static final String AGREEMENT_KEYPREFIX = "transferAgreement";
private enum AssetTransferErrors {
INCOMPLETE_INPUT,
INVALID_ACCESS,
ASSET_NOT_FOUND,
ASSET_ALREADY_EXISTS
}
/**
* Retrieves the asset public details with the specified ID from the AssetCollection.
*
* @param ctx the transaction context
* @param assetID the ID of the asset
* @return the asset found on the ledger if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset ReadAsset(final Context ctx, final String assetID) {
ChaincodeStub stub = ctx.getStub();
System.out.printf("ReadAsset: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID);
byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID);
if (assetJSON == null || assetJSON.length == 0) {
System.out.printf("Asset not found: ID %s\n", assetID);
return null;
}
Asset asset = Asset.deserialize(assetJSON);
return asset;
}
/**
* Retrieves the asset's AssetPrivateDetails details with the specified ID from the Collection.
*
* @param ctx the transaction context
* @param collection the org's collection containing asset private details
* @param assetID the ID of the asset
* @return the AssetPrivateDetails from the collection, if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public AssetPrivateDetails ReadAssetPrivateDetails(final Context ctx, final String collection, final String assetID) {
ChaincodeStub stub = ctx.getStub();
System.out.printf("ReadAssetPrivateDetails: collection %s, ID %s\n", collection, assetID);
byte[] assetPrvJSON = stub.getPrivateData(collection, assetID);
if (assetPrvJSON == null || assetPrvJSON.length == 0) {
String errorMessage = String.format("AssetPrivateDetails %s does not exist in collection %s", assetID, collection);
System.out.println(errorMessage);
return null;
}
AssetPrivateDetails assetpd = AssetPrivateDetails.deserialize(assetPrvJSON);
return assetpd;
}
/**
* ReadTransferAgreement gets the buyer's identity from the transfer agreement from collection
*
* @param ctx the transaction context
* @param assetID the ID of the asset
* @return the AssetPrivateDetails from the collection, if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public TransferAgreement ReadTransferAgreement(final Context ctx, final String assetID) {
ChaincodeStub stub = ctx.getStub();
CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID);
System.out.printf("ReadTransferAgreement Get: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey);
byte[] buyerIdentity = stub.getPrivateData(ASSET_COLLECTION_NAME, aggKey.toString());
if (buyerIdentity == null || buyerIdentity.length == 0) {
String errorMessage = String.format("BuyerIdentity for asset %s does not exist in TransferAgreement ", assetID);
System.out.println(errorMessage);
return null;
}
return new TransferAgreement(assetID, new String(buyerIdentity, UTF_8));
}
/**
* 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 collection, since transaction may not get endorsed
* on some peers that do not have the collection.
*
* @param ctx the transaction context
* @param startKey for ID range of the asset
* @param endKey for ID range of the asset
* @return the asset found on the ledger if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset[] GetAssetByRange(final Context ctx, final String startKey, final String endKey) throws Exception {
ChaincodeStub stub = ctx.getStub();
System.out.printf("GetAssetByRange: start %s, end %s\n", startKey, endKey);
List<Asset> queryResults = new ArrayList<>();
// retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order.
try (QueryResultsIterator<KeyValue> results = stub.getPrivateDataByRange(ASSET_COLLECTION_NAME, startKey, endKey)) {
for (KeyValue result : results) {
if (result.getStringValue() == null || result.getStringValue().length() == 0) {
System.err.printf("Invalid Asset json: %s\n", result.getStringValue());
continue;
}
Asset asset = Asset.deserialize(result.getStringValue());
queryResults.add(asset);
System.out.println("QueryResult: " + asset.toString());
}
}
return queryResults.toArray(new Asset[0]);
}
// =======Rich queries =========================================================================
// Two examples of rich queries are provided below (parameterized query and ad hoc query).
// Rich queries pass a query string to the state database.
// Rich queries are only supported by state database implementations
// that support rich query (e.g. CouchDB).
// The query string is in the syntax of the underlying state database.
// With rich queries there is no guarantee that the result set hasn't changed between
// endorsement time and commit time, aka 'phantom reads'.
// Therefore, rich queries should not be used in update transactions, unless the
// application handles the possibility of result set changes between endorsement and commit time.
// Rich queries can be used for point-in-time queries against a peer.
// ============================================================================================
/**
* QueryAssetByOwner queries for assets based on assetType, owner.
* This is an example of a parameterized query where the query logic is baked into the chaincode,
* and accepting a single query parameter (owner).
*
* @param ctx the transaction context
* @param assetType type to query for
* @param owner asset owner to query for
* @return the asset found on the ledger if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset[] QueryAssetByOwner(final Context ctx, final String assetType, final String owner) throws Exception {
String queryString = String.format("{\"selector\":{\"objectType\":\"%s\",\"owner\":\"%s\"}}", assetType, owner);
return getQueryResult(ctx, queryString);
}
/**
* QueryAssets uses a query string to perform a query for assets.
* Query string matching state database syntax is passed in and executed as is.
* Supports ad hoc queries that can be defined at runtime by the client.
*
* @param ctx the transaction context
* @param queryString query string matching state database syntax
* @return the asset found on the ledger if there was one
*/
@Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset[] QueryAssets(final Context ctx, final String queryString) throws Exception {
return getQueryResult(ctx, queryString);
}
private Asset[] getQueryResult(final Context ctx, final String queryString) throws Exception {
ChaincodeStub stub = ctx.getStub();
System.out.printf("QueryAssets: %s\n", queryString);
List<Asset> queryResults = new ArrayList<Asset>();
// retrieve asset with keys between startKey (inclusive) and endKey(exclusive) in lexical order.
try (QueryResultsIterator<KeyValue> results = stub.getPrivateDataQueryResult(ASSET_COLLECTION_NAME, queryString)) {
for (KeyValue result : results) {
if (result.getStringValue() == null || result.getStringValue().length() == 0) {
System.err.printf("Invalid Asset json: %s\n", result.getStringValue());
continue;
}
Asset asset = Asset.deserialize(result.getStringValue());
queryResults.add(asset);
System.out.println("QueryResult: " + asset.toString());
}
}
return queryResults.toArray(new Asset[0]);
}
/**
* Creates a new asset on the ledger from asset properties passed in as transient map.
* Asset owner will be inferred from the ClientId via stub api
*
* @param ctx the transaction context
* Transient map with asset_properties key with asset json as value
* @return the created asset
*/
@Transaction(intent = Transaction.TYPE.SUBMIT)
public Asset CreateAsset(final Context ctx) {
ChaincodeStub stub = ctx.getStub();
Map<String, byte[]> transientMap = ctx.getStub().getTransient();
if (!transientMap.containsKey("asset_properties")) {
String errorMessage = String.format("CreateAsset call must specify asset_properties in Transient map input");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
byte[] transientAssetJSON = transientMap.get("asset_properties");
final String assetID;
final String type;
final String color;
int appraisedValue = 0;
int size = 0;
try {
JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
Map<String, Object> tMap = json.toMap();
type = (String) tMap.get("objectType");
assetID = (String) tMap.get("assetID");
color = (String) tMap.get("color");
if (tMap.containsKey("size")) {
size = (Integer) tMap.get("size");
}
if (tMap.containsKey("appraisedValue")) {
appraisedValue = (Integer) tMap.get("appraisedValue");
}
} catch (Exception err) {
String errorMessage = String.format("TransientMap deserialized error: %s ", err);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
//input validations
String errorMessage = null;
if (assetID.equals("")) {
errorMessage = String.format("Empty input in Transient map: assetID");
}
if (type.equals("")) {
errorMessage = String.format("Empty input in Transient map: objectType");
}
if (color.equals("")) {
errorMessage = String.format("Empty input in Transient map: color");
}
if (size <= 0) {
errorMessage = String.format("Empty input in Transient map: size");
}
if (appraisedValue <= 0) {
errorMessage = String.format("Empty input in Transient map: appraisedValue");
}
if (errorMessage != null) {
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
Asset asset = new Asset(type, assetID, color, size, "");
// Check if asset already exists
byte[] assetJSON = ctx.getStub().getPrivateData(ASSET_COLLECTION_NAME, assetID);
if (assetJSON != null && assetJSON.length > 0) {
errorMessage = String.format("Asset %s already exists", assetID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString());
}
// Get ID of submitting client identity
String clientID = ctx.getClientIdentity().getId();
// Verify that the client is submitting request to peer in their organization
// This is to ensure that a client from another org doesn't attempt to read or
// write private data from this peer.
verifyClientOrgMatchesPeerOrg(ctx);
//Make submitting client the owner
asset.setOwner(clientID);
System.out.printf("CreateAsset Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID);
System.out.printf("Put: collection %s, ID %s\n", ASSET_COLLECTION_NAME, new String(asset.serialize()));
stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, asset.serialize());
// Get collection name for this organization.
String orgCollectionName = getCollectionName(ctx);
//Save AssetPrivateDetails to org collection
AssetPrivateDetails assetPriv = new AssetPrivateDetails(assetID, appraisedValue);
System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID);
stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize());
return asset;
}
/**
* AgreeToTransfer is used by the potential buyer of the asset to agree to the
* asset value. The agreed to appraisal value is stored in the buying orgs
* org specifc collection, while the the buyer client ID is stored in the asset collection
* using a composite key
* Uses transient map with key asset_value
*
* @param ctx the transaction context
*/
@Transaction(intent = Transaction.TYPE.SUBMIT)
public void AgreeToTransfer(final Context ctx) {
ChaincodeStub stub = ctx.getStub();
Map<String, byte[]> transientMap = ctx.getStub().getTransient();
if (!transientMap.containsKey("asset_value")) {
String errorMessage = String.format("AgreeToTransfer call must specify \"asset_value\" in Transient map input");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
byte[] transientAssetJSON = transientMap.get("asset_value");
AssetPrivateDetails assetPriv;
String assetID;
try {
JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
assetID = json.getString("assetID");
final int appraisedValue = json.getInt("appraisedValue");
assetPriv = new AssetPrivateDetails(assetID, appraisedValue);
} catch (Exception err) {
String errorMessage = String.format("TransientMap deserialized error %s ", err);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
if (assetID.equals("")) {
String errorMessage = String.format("Invalid input in Transient map: assetID");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
if (assetPriv.getAppraisedValue() <= 0) { // appraisedValue field must be a positive integer
String errorMessage = String.format("Input must be positive integer: appraisedValue");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
System.out.printf("AgreeToTransfer: verify asset %s exists\n", assetID);
Asset existing = ReadAsset(ctx, assetID);
if (existing == null) {
String errorMessage = String.format("Asset does not exist in the collection: ", assetID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
// Get collection name for this organization.
String orgCollectionName = getCollectionName(ctx);
verifyClientOrgMatchesPeerOrg(ctx);
//Save AssetPrivateDetails to org collection
System.out.printf("Put AssetPrivateDetails: collection %s, ID %s\n", orgCollectionName, assetID);
stub.putPrivateData(orgCollectionName, assetID, assetPriv.serialize());
String clientID = ctx.getClientIdentity().getId();
//Write the AgreeToTransfer key in assetCollection
CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID);
System.out.printf("AgreeToTransfer Put: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey);
stub.putPrivateData(ASSET_COLLECTION_NAME, aggKey.toString(), clientID);
}
/**
* TransferAsset transfers the asset to the new owner by setting a new owner ID based on
* AgreeToTransfer data
*
* @param ctx the transaction context
* @return none
*/
@Transaction(intent = Transaction.TYPE.SUBMIT)
public void TransferAsset(final Context ctx) {
ChaincodeStub stub = ctx.getStub();
Map<String, byte[]> transientMap = ctx.getStub().getTransient();
if (!transientMap.containsKey("asset_owner")) {
String errorMessage = "TransferAsset call must specify \"asset_owner\" in Transient map input";
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
byte[] transientAssetJSON = transientMap.get("asset_owner");
final String assetID;
final String buyerMSP;
try {
JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
assetID = json.getString("assetID");
buyerMSP = json.getString("buyerMSP");
} catch (Exception err) {
String errorMessage = String.format("TransientMap deserialized error %s ", err);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
if (assetID.equals("")) {
String errorMessage = String.format("Invalid input in Transient map: " + "assetID");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
if (buyerMSP.equals("")) {
String errorMessage = String.format("Invalid input in Transient map: " + "buyerMSP");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
System.out.printf("TransferAsset: verify asset %s exists\n", assetID);
byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID);
if (assetJSON == null || assetJSON.length == 0) {
String errorMessage = String.format("Asset %s does not exist in the collection", assetID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
verifyClientOrgMatchesPeerOrg(ctx);
Asset thisAsset = Asset.deserialize(assetJSON);
// Verify transfer details and transfer owner
verifyAgreement(ctx, assetID, thisAsset.getOwner(), buyerMSP);
TransferAgreement transferAgreement = ReadTransferAgreement(ctx, assetID);
if (transferAgreement == null) {
String errorMessage = String.format("TransferAgreement does not exist for asset: %s", assetID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
// Transfer asset in private data collection to new owner
String newOwner = transferAgreement.getBuyerID();
thisAsset.setOwner(newOwner);
//Save updated Asset to collection
System.out.printf("Transfer Asset: collection %s, ID %s to owner %s\n", ASSET_COLLECTION_NAME, assetID, newOwner);
stub.putPrivateData(ASSET_COLLECTION_NAME, assetID, thisAsset.serialize());
// delete the key from owners collection
String ownersCollectionName = getCollectionName(ctx);
stub.delPrivateData(ownersCollectionName, assetID);
//Delete the transfer agreement from the asset collection
CompositeKey aggKey = stub.createCompositeKey(AGREEMENT_KEYPREFIX, assetID);
System.out.printf("AgreeToTransfer deleteKey: collection %s, ID %s, Key %s\n", ASSET_COLLECTION_NAME, assetID, aggKey);
stub.delPrivateData(ASSET_COLLECTION_NAME, aggKey.toString());
}
/**
* Deletes a asset & related details from the ledger.
* Input in transient map: asset_delete
*
* @param ctx the transaction context
*/
@Transaction(intent = Transaction.TYPE.SUBMIT)
public void DeleteAsset(final Context ctx) {
ChaincodeStub stub = ctx.getStub();
Map<String, byte[]> transientMap = ctx.getStub().getTransient();
if (!transientMap.containsKey("asset_delete")) {
String errorMessage = String.format("DeleteAsset call must specify 'asset_delete' in Transient map input");
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
byte[] transientAssetJSON = transientMap.get("asset_delete");
final String assetID;
try {
JSONObject json = new JSONObject(new String(transientAssetJSON, UTF_8));
assetID = json.getString("assetID");
} catch (Exception err) {
String errorMessage = String.format("TransientMap deserialized error: %s ", err);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INCOMPLETE_INPUT.toString());
}
System.out.printf("DeleteAsset: verify asset %s exists\n", assetID);
byte[] assetJSON = stub.getPrivateData(ASSET_COLLECTION_NAME, assetID);
if (assetJSON == null || assetJSON.length == 0) {
String errorMessage = String.format("Asset %s does not exist", assetID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
}
String ownersCollectionName = getCollectionName(ctx);
byte[] apdJSON = stub.getPrivateData(ownersCollectionName, assetID);
if (apdJSON == null || apdJSON.length == 0) {
String errorMessage = String.format("Failed to read asset from owner's Collection %s", ownersCollectionName);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
}
verifyClientOrgMatchesPeerOrg(ctx);
// delete the key from asset collection
System.out.printf("DeleteAsset: collection %s, ID %s\n", ASSET_COLLECTION_NAME, assetID);
stub.delPrivateData(ASSET_COLLECTION_NAME, assetID);
// Finally, delete private details of asset
stub.delPrivateData(ownersCollectionName, assetID);
}
// 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
private void verifyAgreement(final Context ctx, final String assetID, final String owner, final String buyerMSP) {
String clienID = ctx.getClientIdentity().getId();
// Check 1: verify that the transfer is being initiatied by the owner
if (!clienID.equals(owner)) {
throw new ChaincodeException("Submitting client identity does not own the asset", AssetTransferErrors.INVALID_ACCESS.toString());
}
// Check 2: verify that the buyer has agreed to the appraised value
String collectionOwner = getCollectionName(ctx); // get owner collection from caller identity
String collectionBuyer = buyerMSP + "PrivateCollection";
// Get hash of owners agreed to value
byte[] ownerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionOwner, assetID);
if (ownerAppraisedValueHash == null) {
throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s", assetID, collectionOwner));
}
// Get hash of buyers agreed to value
byte[] buyerAppraisedValueHash = ctx.getStub().getPrivateDataHash(collectionBuyer, assetID);
if (buyerAppraisedValueHash == null) {
throw new ChaincodeException(String.format("Hash of appraised value for %s does not exist in collection %s. AgreeToTransfer must be called by the buyer first.", assetID, collectionBuyer));
}
// Verify that the two hashes match
if (!Arrays.equals(ownerAppraisedValueHash, buyerAppraisedValueHash)) {
throw new ChaincodeException(String.format("Hash for appraised value for owner %x does not match value for seller %x", ownerAppraisedValueHash, buyerAppraisedValueHash));
}
}
private void verifyClientOrgMatchesPeerOrg(final Context ctx) {
String clientMSPID = ctx.getClientIdentity().getMSPID();
String peerMSPID = ctx.getStub().getMspId();
if (!peerMSPID.equals(clientMSPID)) {
String errorMessage = String.format("Client from org %s is not authorized to read or write private data from an org %s peer", clientMSPID, peerMSPID);
System.err.println(errorMessage);
throw new ChaincodeException(errorMessage, AssetTransferErrors.INVALID_ACCESS.toString());
}
}
private String getCollectionName(final Context ctx) {
// Get the MSP ID of submitting client identity
String clientMSPID = ctx.getClientIdentity().getMSPID();
// Create the collection name
return clientMSPID + "PrivateCollection";
}
}