-
Notifications
You must be signed in to change notification settings - Fork 3.4k
/
Copy pathasset_transfer_ledger_chaincode.js
411 lines (351 loc) · 16.1 KB
/
asset_transfer_ledger_chaincode.js
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
/*
* Copyright IBM Corp. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
// ====CHAINCODE EXECUTION SAMPLES (CLI) ==================
// ==== Invoke assets ====
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","35","Tom","100"]}'
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","50","Tom","150"]}'
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","70","Tom","200"]}'
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}'
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAssetByColor","blue","jerry"]}'
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}'
// ==== Query assets ====
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}'
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}'
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}'
// Rich Query (Only supported if CouchDB is used as state database):
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsByOwner","Tom"]}' output issue
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"owner\":\"Tom\"}}"]}'
// Rich Query with Pagination (Only supported if CouchDB is used as state database):
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsWithPagination","{\"selector\":{\"owner\":\"Tom\"}}","3",""]}'
// INDEXES TO SUPPORT COUCHDB RICH QUERIES
//
// Indexes in CouchDB are required in order to make JSON queries efficient and are required for
// any JSON query with a sort. Indexes may be packaged alongside
// chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own
// text file with extension *.json with the index definition formatted in JSON following the
// CouchDB index JSON syntax as documented at:
// http://docs.couchdb.org/en/2.3.1/api/database/find.html#db-index
//
// This asset transfer ledger example chaincode demonstrates a packaged
// index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json.
//
// If you have access to the your peer's CouchDB state database in a development environment,
// you may want to iteratively test various indexes in support of your chaincode queries. You
// can use the CouchDB Fauxton interface or a command line curl utility to create and update
// indexes. Then once you finalize an index, include the index definition alongside your
// chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment
// to managed environments.
//
// In the examples below you can find index definitions that support asset transfer ledger
// chaincode queries, along with the syntax that you can use in development environments
// to create the indexes in the CouchDB Fauxton interface or a curl command line utility.
//
// Index for docType, owner.
//
// Example curl command line to define index in the CouchDB channel_chaincode database
// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
//
// Index for docType, owner, size (descending order).
//
// Example curl command line to define index in the CouchDB channel_chaincode database
// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database):
// peer chaincode query -C CHANNEL_NAME -n ledger -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}'
// Rich Query with index design doc specified only (Only supported if CouchDB is used as state database):
// peer chaincode query -C CHANNEL_NAME -n ledger -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"Tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}'
'use strict';
const {Contract} = require('fabric-contract-api');
class Chaincode extends Contract {
// CreateAsset - create a new asset, store into chaincode state
async CreateAsset(ctx, assetID, color, size, owner, appraisedValue) {
const exists = await this.AssetExists(ctx, assetID);
if (exists) {
throw new Error(`The asset ${assetID} already exists`);
}
// ==== Create asset object and marshal to JSON ====
let asset = {
docType: 'asset',
assetID: assetID,
color: color,
size: size,
owner: owner,
appraisedValue: appraisedValue
};
// === Save asset to state ===
await ctx.stub.putState(assetID, Buffer.from(JSON.stringify(asset)));
let indexName = 'color~name';
let colorNameIndexKey = await ctx.stub.createCompositeKey(indexName, [asset.color, asset.assetID]);
// Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
await ctx.stub.putState(colorNameIndexKey, Buffer.from('\u0000'));
}
// ReadAsset returns the asset stored in the world state with given id.
async ReadAsset(ctx, id) {
const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state
if (!assetJSON || assetJSON.length === 0) {
throw new Error(`Asset ${id} does not exist`);
}
return assetJSON.toString();
}
// delete - remove a asset key/value pair from state
async DeleteAsset(ctx, id) {
if (!id) {
throw new Error('Asset name must not be empty');
}
let exists = await this.AssetExists(ctx, id);
if (!exists) {
throw new Error(`Asset ${id} does not exist`);
}
// to maintain the color~name index, we need to read the asset first and get its color
let valAsbytes = await ctx.stub.getState(id); // get the asset from chaincode state
let jsonResp = {};
if (!valAsbytes) {
jsonResp.error = `Asset does not exist: ${id}`;
throw new Error(jsonResp);
}
let assetJSON;
try {
assetJSON = JSON.parse(valAsbytes.toString());
} catch (err) {
jsonResp = {};
jsonResp.error = `Failed to decode JSON of: ${id}`;
throw new Error(jsonResp);
}
await ctx.stub.deleteState(id); //remove the asset from chaincode state
// delete the index
let indexName = 'color~name';
let colorNameIndexKey = ctx.stub.createCompositeKey(indexName, [assetJSON.color, assetJSON.assetID]);
if (!colorNameIndexKey) {
throw new Error(' Failed to create the createCompositeKey');
}
// Delete index entry to state.
await ctx.stub.deleteState(colorNameIndexKey);
}
// TransferAsset transfers a asset by setting a new owner name on the asset
async TransferAsset(ctx, assetName, newOwner) {
let assetAsBytes = await ctx.stub.getState(assetName);
if (!assetAsBytes || !assetAsBytes.toString()) {
throw new Error(`Asset ${assetName} does not exist`);
}
let assetToTransfer = {};
try {
assetToTransfer = JSON.parse(assetAsBytes.toString()); //unmarshal
} catch (err) {
let jsonResp = {};
jsonResp.error = 'Failed to decode JSON of: ' + assetName;
throw new Error(jsonResp);
}
assetToTransfer.owner = newOwner; //change the owner
let assetJSONasBytes = Buffer.from(JSON.stringify(assetToTransfer));
await ctx.stub.putState(assetName, assetJSONasBytes); //rewrite the asset
}
// GetAssetsByRange performs a range query based on the start and end keys provided.
// Read-only function results are not typically submitted to ordering. If the read-only
// results are submitted to ordering, or if the query is used in an update transaction
// and submitted to ordering, then the committing peers will re-execute to guarantee that
// result sets are stable between endorsement time and commit time. The transaction is
// invalidated by the committing peers if the result set has changed between endorsement
// time and commit time.
// Therefore, range queries are a safe option for performing update transactions based on query results.
async GetAssetsByRange(ctx, startKey, endKey) {
let resultsIterator = await ctx.stub.getStateByRange(startKey, endKey);
let results = await this._GetAllResults(resultsIterator, false);
return JSON.stringify(results);
}
// TransferAssetByColor will transfer assets of a given color to a certain new owner.
// Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'.
// Committing peers will re-execute range queries to guarantee that result sets are stable
// between endorsement time and commit time. The transaction is invalidated by the
// committing peers if the result set has changed between endorsement time and commit time.
// Therefore, range queries are a safe option for performing update transactions based on query results.
// Example: GetStateByPartialCompositeKey/RangeQuery
async TransferAssetByColor(ctx, color, newOwner) {
// Query the color~name index by color
// This will execute a key range query on all keys starting with 'color'
let coloredAssetResultsIterator = await ctx.stub.getStateByPartialCompositeKey('color~name', [color]);
// Iterate through result set and for each asset found, transfer to newOwner
let responseRange = await coloredAssetResultsIterator.next();
while (!responseRange.done) {
if (!responseRange || !responseRange.value || !responseRange.value.key) {
return;
}
let objectType;
let attributes;
(
{objectType, attributes} = await ctx.stub.splitCompositeKey(responseRange.value.key)
);
console.log(objectType);
let returnedAssetName = attributes[1];
// Now call the transfer function for the found asset.
// Re-use the same function that is used to transfer individual assets
await this.TransferAsset(ctx, returnedAssetName, newOwner);
responseRange = await coloredAssetResultsIterator.next();
}
}
// QueryAssetsByOwner queries for assets based on a passed in 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).
// Only available on state databases that support rich query (e.g. CouchDB)
// Example: Parameterized rich query
async QueryAssetsByOwner(ctx, owner) {
let queryString = {};
queryString.selector = {};
queryString.selector.docType = 'asset';
queryString.selector.owner = owner;
return await this.GetQueryResultForQueryString(ctx, JSON.stringify(queryString)); //shim.success(queryResults);
}
// Example: Ad hoc rich query
// 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.
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
// Only available on state databases that support rich query (e.g. CouchDB)
async QueryAssets(ctx, queryString) {
return await this.GetQueryResultForQueryString(ctx, queryString);
}
// GetQueryResultForQueryString executes the passed in query string.
// Result set is built and returned as a byte array containing the JSON results.
async GetQueryResultForQueryString(ctx, queryString) {
let resultsIterator = await ctx.stub.getQueryResult(queryString);
let results = await this._GetAllResults(resultsIterator, false);
return JSON.stringify(results);
}
// Example: Pagination with Range Query
// GetAssetsByRangeWithPagination performs a range query based on the start & end key,
// page size and a bookmark.
// The number of fetched records will be equal to or lesser than the page size.
// Paginated range queries are only valid for read only transactions.
async GetAssetsByRangeWithPagination(ctx, startKey, endKey, pageSize, bookmark) {
const {iterator, metadata} = await ctx.stub.getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark);
let results = {};
results.results = await this._GetAllResults(iterator, false);
results.fetchedRecordsCount = metadata.fetchedRecordsCount;
results.bookmark = metadata.bookmark;
return JSON.stringify(results);
}
// Example: Pagination with Ad hoc Rich Query
// QueryAssetsWithPagination uses a query string, page size and a bookmark to perform a query
// for assets. Query string matching state database syntax is passed in and executed as is.
// The number of fetched records would be equal to or lesser than the specified page size.
// Supports ad hoc queries that can be defined at runtime by the client.
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
// Only available on state databases that support rich query (e.g. CouchDB)
// Paginated queries are only valid for read only transactions.
async QueryAssetsWithPagination(ctx, queryString, pageSize, bookmark) {
const {iterator, metadata} = await ctx.stub.getQueryResultWithPagination(queryString, pageSize, bookmark);
let results = {};
results.results = await this._GetAllResults(iterator, false);
results.fetchedRecordsCount = metadata.fetchedRecordsCount;
results.bookmark = metadata.bookmark;
return JSON.stringify(results);
}
// GetAssetHistory returns the chain of custody for an asset since issuance.
async GetAssetHistory(ctx, assetName) {
let resultsIterator = await ctx.stub.getHistoryForKey(assetName);
let results = await this._GetAllResults(resultsIterator, true);
return JSON.stringify(results);
}
// AssetExists returns true when asset with given ID exists in world state
async AssetExists(ctx, assetName) {
// ==== Check if asset already exists ====
let assetState = await ctx.stub.getState(assetName);
return assetState && assetState.length > 0;
}
// This is JavaScript so without Funcation Decorators, all functions are assumed
// to be transaction functions
//
// For internal functions... prefix them with _
async _GetAllResults(iterator, isHistory) {
let allResults = [];
let res = await iterator.next();
while (!res.done) {
if (res.value && res.value.value.toString()) {
let jsonRes = {};
console.log(res.value.value.toString('utf8'));
if (isHistory && isHistory === true) {
jsonRes.TxId = res.value.txId;
jsonRes.Timestamp = res.value.timestamp;
try {
jsonRes.Value = JSON.parse(res.value.value.toString('utf8'));
} catch (err) {
console.log(err);
jsonRes.Value = res.value.value.toString('utf8');
}
} else {
jsonRes.Key = res.value.key;
try {
jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
} catch (err) {
console.log(err);
jsonRes.Record = res.value.value.toString('utf8');
}
}
allResults.push(jsonRes);
}
res = await iterator.next();
}
iterator.close();
return allResults;
}
// InitLedger creates sample assets in the ledger
async InitLedger(ctx) {
const assets = [
{
assetID: 'asset1',
color: 'blue',
size: 5,
owner: 'Tom',
appraisedValue: 100
},
{
assetID: 'asset2',
color: 'red',
size: 5,
owner: 'Brad',
appraisedValue: 100
},
{
assetID: 'asset3',
color: 'green',
size: 10,
owner: 'Jin Soo',
appraisedValue: 200
},
{
assetID: 'asset4',
color: 'yellow',
size: 10,
owner: 'Max',
appraisedValue: 200
},
{
assetID: 'asset5',
color: 'black',
size: 15,
owner: 'Adriana',
appraisedValue: 250
},
{
assetID: 'asset6',
color: 'white',
size: 15,
owner: 'Michel',
appraisedValue: 250
},
];
for (const asset of assets) {
await this.CreateAsset(
ctx,
asset.assetID,
asset.color,
asset.size,
asset.owner,
asset.appraisedValue
);
}
}
}
module.exports = Chaincode;