mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
-Go chaincode - Paginated range query should return the bookmark so that next page can be requested -Javascript chaincode - Make query return JSON consistent with Go chaincode -Javascript app was broken at bookmark query due to invalid JSON parsing from inconsistent chaincode responses -Javascript and Java app had incorrect comments Signed-off-by: David Enyeart <enyeart@us.ibm.com>
475 lines
19 KiB
Go
475 lines
19 KiB
Go
/*
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/*
|
|
====CHAINCODE EXECUTION SAMPLES (CLI) ==================
|
|
|
|
==== Invoke assets ====
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","5","tom","35"]}'
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","4","tom","50"]}'
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","6","tom","70"]}'
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}'
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAssetByColor","blue","jerry"]}'
|
|
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}'
|
|
|
|
==== Query assets ====
|
|
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}'
|
|
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}'
|
|
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}'
|
|
|
|
Rich Query (Only supported if CouchDB is used as state database):
|
|
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssetsByOwner","tom"]}'
|
|
peer chaincode query -C myc1 -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 myc1 -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 myc1 -n asset_transfer -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 myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}'
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
"github.com/hyperledger/fabric-chaincode-go/shim"
|
|
"github.com/hyperledger/fabric-contract-api-go/contractapi"
|
|
)
|
|
|
|
const index = "color~name"
|
|
|
|
// SimpleChaincode implements the fabric-contract-api-go programming model
|
|
type SimpleChaincode struct {
|
|
contractapi.Contract
|
|
}
|
|
|
|
type Asset struct {
|
|
DocType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
|
|
ID string `json:"ID"` //the field tags are needed to keep case from bouncing around
|
|
Color string `json:"color"`
|
|
Size int `json:"size"`
|
|
Owner string `json:"owner"`
|
|
AppraisedValue int `json:"appraisedValue"`
|
|
}
|
|
|
|
// HistoryQueryResult structure used for returning result of history query
|
|
type HistoryQueryResult struct {
|
|
Record *Asset `json:"record"`
|
|
TxId string `json:"txId"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
IsDelete bool `json:"isDelete"`
|
|
}
|
|
|
|
// PaginatedQueryResult structure used for returning paginated query results and metadata
|
|
type PaginatedQueryResult struct {
|
|
Records []*Asset `json:"records"`
|
|
FetchedRecordsCount int32 `json:"fetchedRecordsCount"`
|
|
Bookmark string `json:"bookmark"`
|
|
}
|
|
|
|
// CreateAsset initializes a new asset in the ledger
|
|
func (t *SimpleChaincode) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, color string, size int, owner string, appraisedValue int) error {
|
|
exists, err := t.AssetExists(ctx, assetID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get asset: %v", err)
|
|
}
|
|
if exists {
|
|
return fmt.Errorf("asset already exists: %s", assetID)
|
|
}
|
|
|
|
asset := &Asset{
|
|
DocType: "asset",
|
|
ID: assetID,
|
|
Color: color,
|
|
Size: size,
|
|
Owner: owner,
|
|
AppraisedValue: appraisedValue,
|
|
}
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(assetID, assetBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create an index to enable color-based range queries, e.g. return all blue assets.
|
|
// An 'index' is a normal key-value entry in the ledger.
|
|
// The key is a composite key, with the elements that you want to range query on listed first.
|
|
// In our case, the composite key is based on indexName~color~name.
|
|
// This will enable very efficient state range queries based on composite keys matching indexName~color~*
|
|
colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Save index entry to world state. Only the key name is needed, no need to store a duplicate copy of the asset.
|
|
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
|
|
value := []byte{0x00}
|
|
return ctx.GetStub().PutState(colorNameIndexKey, value)
|
|
}
|
|
|
|
// ReadAsset retrieves an asset from the ledger
|
|
func (t *SimpleChaincode) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) {
|
|
assetBytes, err := ctx.GetStub().GetState(assetID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get asset %s: %v", assetID, err)
|
|
}
|
|
if assetBytes == nil {
|
|
return nil, fmt.Errorf("asset %s does not exist", assetID)
|
|
}
|
|
|
|
var asset Asset
|
|
err = json.Unmarshal(assetBytes, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &asset, nil
|
|
}
|
|
|
|
// DeleteAsset removes an asset key-value pair from the ledger
|
|
func (t *SimpleChaincode) DeleteAsset(ctx contractapi.TransactionContextInterface, assetID string) error {
|
|
asset, err := t.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().DelState(assetID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete asset %s: %v", assetID, err)
|
|
}
|
|
|
|
colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete index entry
|
|
return ctx.GetStub().DelState(colorNameIndexKey)
|
|
}
|
|
|
|
// TransferAsset transfers an asset by setting a new owner name on the asset
|
|
func (t *SimpleChaincode) TransferAsset(ctx contractapi.TransactionContextInterface, assetID, newOwner string) error {
|
|
asset, err := t.ReadAsset(ctx, assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
asset.Owner = newOwner
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.GetStub().PutState(assetID, assetBytes)
|
|
}
|
|
|
|
// constructQueryResponseFromIterator constructs a slice of assets from the resultsIterator
|
|
func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) ([]*Asset, error) {
|
|
var assets []*Asset
|
|
for resultsIterator.HasNext() {
|
|
queryResult, err := resultsIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var asset Asset
|
|
err = json.Unmarshal(queryResult.Value, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assets = append(assets, &asset)
|
|
}
|
|
|
|
return assets, nil
|
|
}
|
|
|
|
// 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.
|
|
func (t *SimpleChaincode) GetAssetsByRange(ctx contractapi.TransactionContextInterface, startKey, endKey string) ([]*Asset, error) {
|
|
resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
return constructQueryResponseFromIterator(resultsIterator)
|
|
}
|
|
|
|
// TransferAssetByColor will transfer assets of a given color to a certain new owner.
|
|
// Uses 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
|
|
func (t *SimpleChaincode) TransferAssetByColor(ctx contractapi.TransactionContextInterface, color, newOwner string) error {
|
|
// Execute a key range query on all keys starting with 'color'
|
|
coloredAssetResultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey(index, []string{color})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer coloredAssetResultsIterator.Close()
|
|
|
|
for coloredAssetResultsIterator.HasNext() {
|
|
responseRange, err := coloredAssetResultsIterator.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, compositeKeyParts, err := ctx.GetStub().SplitCompositeKey(responseRange.Key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(compositeKeyParts) > 1 {
|
|
returnedAssetID := compositeKeyParts[1]
|
|
asset, err := t.ReadAsset(ctx, returnedAssetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
asset.Owner = newOwner
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ctx.GetStub().PutState(returnedAssetID, assetBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("transfer failed for asset %s: %v", returnedAssetID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// QueryAssetsByOwner queries for assets based on the owners name.
|
|
// 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
|
|
func (t *SimpleChaincode) QueryAssetsByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]*Asset, error) {
|
|
queryString := fmt.Sprintf(`{"selector":{"docType":"asset","owner":"%s"}}`, owner)
|
|
return getQueryResultForQueryString(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.
|
|
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
|
|
// Only available on state databases that support rich query (e.g. CouchDB)
|
|
// Example: Ad hoc rich query
|
|
func (t *SimpleChaincode) QueryAssets(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) {
|
|
return getQueryResultForQueryString(ctx, queryString)
|
|
}
|
|
|
|
// getQueryResultForQueryString executes the passed in query string.
|
|
// The result set is built and returned as a byte array containing the JSON results.
|
|
func getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) {
|
|
resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
return constructQueryResponseFromIterator(resultsIterator)
|
|
}
|
|
|
|
// GetAssetsByRangeWithPagination performs a range query based on the start and 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.
|
|
// Example: Pagination with Range Query
|
|
func (t *SimpleChaincode) GetAssetsByRangeWithPagination(ctx contractapi.TransactionContextInterface, startKey string, endKey string, pageSize int, bookmark string) (*PaginatedQueryResult, error) {
|
|
|
|
resultsIterator, responseMetadata, err := ctx.GetStub().GetStateByRangeWithPagination(startKey, endKey, int32(pageSize), bookmark)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
assets, err := constructQueryResponseFromIterator(resultsIterator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PaginatedQueryResult{
|
|
Records: assets,
|
|
FetchedRecordsCount: responseMetadata.FetchedRecordsCount,
|
|
Bookmark: responseMetadata.Bookmark,
|
|
}, nil
|
|
}
|
|
|
|
// 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.
|
|
// Example: Pagination with Ad hoc Rich Query
|
|
func (t *SimpleChaincode) QueryAssetsWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int, bookmark string) (*PaginatedQueryResult, error) {
|
|
|
|
return getQueryResultForQueryStringWithPagination(ctx, queryString, int32(pageSize), bookmark)
|
|
}
|
|
|
|
// getQueryResultForQueryStringWithPagination executes the passed in query string with
|
|
// pagination info. The result set is built and returned as a byte array containing the JSON results.
|
|
func getQueryResultForQueryStringWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int32, bookmark string) (*PaginatedQueryResult, error) {
|
|
|
|
resultsIterator, responseMetadata, err := ctx.GetStub().GetQueryResultWithPagination(queryString, pageSize, bookmark)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
assets, err := constructQueryResponseFromIterator(resultsIterator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PaginatedQueryResult{
|
|
Records: assets,
|
|
FetchedRecordsCount: responseMetadata.FetchedRecordsCount,
|
|
Bookmark: responseMetadata.Bookmark,
|
|
}, nil
|
|
}
|
|
|
|
// GetAssetHistory returns the chain of custody for an asset since issuance.
|
|
func (t *SimpleChaincode) GetAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]HistoryQueryResult, error) {
|
|
log.Printf("GetAssetHistory: ID %v", assetID)
|
|
|
|
resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
var records []HistoryQueryResult
|
|
for resultsIterator.HasNext() {
|
|
response, err := resultsIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var asset Asset
|
|
if len(response.Value) > 0 {
|
|
err = json.Unmarshal(response.Value, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
asset = Asset{
|
|
ID: assetID,
|
|
}
|
|
}
|
|
|
|
timestamp, err := ptypes.Timestamp(response.Timestamp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
record := HistoryQueryResult{
|
|
TxId: response.TxId,
|
|
Timestamp: timestamp,
|
|
Record: &asset,
|
|
IsDelete: response.IsDelete,
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
// AssetExists returns true when asset with given ID exists in the ledger.
|
|
func (t *SimpleChaincode) AssetExists(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
|
|
assetBytes, err := ctx.GetStub().GetState(assetID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read asset %s from world state. %v", assetID, err)
|
|
}
|
|
|
|
return assetBytes != nil, nil
|
|
}
|
|
|
|
// InitLedger creates the initial set of assets in the ledger.
|
|
func (t *SimpleChaincode) InitLedger(ctx contractapi.TransactionContextInterface) error {
|
|
assets := []Asset{
|
|
{DocType: "asset", ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
|
|
{DocType: "asset", ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
|
|
{DocType: "asset", ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
|
|
{DocType: "asset", ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
|
|
{DocType: "asset", ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
|
|
{DocType: "asset", ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
|
|
}
|
|
|
|
for _, asset := range assets {
|
|
err := t.CreateAsset(ctx, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
chaincode, err := contractapi.NewChaincode(&SimpleChaincode{})
|
|
if err != nil {
|
|
log.Panicf("Error creating asset chaincode: %v", err)
|
|
}
|
|
|
|
if err := chaincode.Start(); err != nil {
|
|
log.Panicf("Error starting asset chaincode: %v", err)
|
|
}
|
|
}
|