mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-21 17:15:10 +00:00
363 lines
8.6 KiB
Go
363 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/hyperledger/fabric-chaincode-go/v2/pkg/statebased"
|
|
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
|
|
)
|
|
|
|
type SmartContract struct {
|
|
contractapi.Contract
|
|
}
|
|
|
|
type Asset struct {
|
|
AppraisedValue int `json:"AppraisedValue"`
|
|
Color string `json:"Color"`
|
|
ID string `json:"ID"`
|
|
Owner string `json:"Owner"`
|
|
Size int `json:"Size"`
|
|
}
|
|
|
|
type OwnerIdentifier struct {
|
|
Org string `json:"org"`
|
|
User string `json:"user"`
|
|
}
|
|
|
|
type AssetCreateInput struct {
|
|
ID string `json:"ID"`
|
|
Color string `json:"Color,omitempty"`
|
|
Owner *string `json:"Owner,omitempty"`
|
|
AppraisedValue *int `json:"AppraisedValue,omitempty"`
|
|
Size *int `json:"Size,omitempty"`
|
|
}
|
|
|
|
type AssetUpdateInput struct {
|
|
ID string `json:"ID"`
|
|
Color *string `json:"Color,omitempty"`
|
|
AppraisedValue *int `json:"AppraisedValue,omitempty"`
|
|
Size *int `json:"Size,omitempty"`
|
|
Owner *string `json:"Owner,omitempty"`
|
|
}
|
|
|
|
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, assetJSON string) error {
|
|
input, err := parseCreateInput(assetJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
exists, err := s.AssetExists(ctx, input.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return fmt.Errorf("the asset %s already exists", input.ID)
|
|
}
|
|
|
|
owner, err := clientIdentifier(ctx, input.Owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
asset := Asset{
|
|
ID: input.ID,
|
|
Color: input.Color,
|
|
Size: valueOrDefault(input.Size, 0),
|
|
Owner: marshalOwner(owner),
|
|
AppraisedValue: valueOrDefault(input.AppraisedValue, 0),
|
|
}
|
|
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ctx.GetStub().PutState(asset.ID, assetBytes); err != nil {
|
|
return fmt.Errorf("failed to put asset in world state: %w", err)
|
|
}
|
|
|
|
if err := setEndorsingOrgs(ctx, asset.ID, owner.Org); err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.GetStub().SetEvent("CreateAsset", assetBytes)
|
|
}
|
|
|
|
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
|
|
asset, err := s.readAsset(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return asset, nil
|
|
}
|
|
|
|
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, assetJSON string) error {
|
|
input, err := parseUpdateInput(assetJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
existingAsset, err := s.readAsset(ctx, input.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := hasWritePermission(ctx, existingAsset); err != nil {
|
|
return err
|
|
}
|
|
|
|
updatedAsset := *existingAsset
|
|
if input.Color != nil {
|
|
updatedAsset.Color = *input.Color
|
|
}
|
|
if input.Size != nil {
|
|
updatedAsset.Size = *input.Size
|
|
}
|
|
if input.AppraisedValue != nil {
|
|
updatedAsset.AppraisedValue = *input.AppraisedValue
|
|
}
|
|
// Owner cannot be updated via UpdateAsset. TransferAsset must be used for ownership changes.
|
|
|
|
assetBytes, err := json.Marshal(updatedAsset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ctx.GetStub().PutState(updatedAsset.ID, assetBytes); err != nil {
|
|
return fmt.Errorf("failed to put updated asset in world state: %w", err)
|
|
}
|
|
|
|
clientOrg, err := getClientOrg(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := setEndorsingOrgs(ctx, updatedAsset.ID, clientOrg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ctx.GetStub().SetEvent("UpdateAsset", assetBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
|
|
asset, err := s.readAsset(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := hasWritePermission(ctx, asset); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ctx.GetStub().DelState(id); err != nil {
|
|
return fmt.Errorf("failed to delete asset %s: %w", id, err)
|
|
}
|
|
|
|
return ctx.GetStub().SetEvent("DeleteAsset", []byte(id))
|
|
}
|
|
|
|
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
|
|
assetBytes, err := ctx.GetStub().GetState(id)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read world state: %w", err)
|
|
}
|
|
|
|
return len(assetBytes) > 0, nil
|
|
}
|
|
|
|
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string, newOwnerOrg string) error {
|
|
asset, err := s.readAsset(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := hasWritePermission(ctx, asset); err != nil {
|
|
return err
|
|
}
|
|
|
|
owner := ownerIdentifier(newOwner, newOwnerOrg)
|
|
ownerJSON, err := json.Marshal(owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
asset.Owner = string(ownerJSON)
|
|
|
|
assetBytes, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ctx.GetStub().PutState(id, assetBytes); err != nil {
|
|
return fmt.Errorf("failed to update asset owner in world state: %w", err)
|
|
}
|
|
|
|
if err := setEndorsingOrgs(ctx, id, newOwnerOrg); err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.GetStub().SetEvent("TransferAsset", assetBytes)
|
|
}
|
|
|
|
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]Asset, error) {
|
|
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
var assets []Asset
|
|
for resultsIterator.HasNext() {
|
|
queryResponse, err := resultsIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var asset Asset
|
|
if err := json.Unmarshal(queryResponse.Value, &asset); err != nil {
|
|
continue
|
|
}
|
|
assets = append(assets, asset)
|
|
}
|
|
|
|
return assets, nil
|
|
}
|
|
|
|
func (s *SmartContract) readAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
|
|
assetBytes, err := ctx.GetStub().GetState(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read world state: %w", err)
|
|
}
|
|
if assetBytes == nil || len(assetBytes) == 0 {
|
|
return nil, fmt.Errorf("sorry, asset %s has not been created", id)
|
|
}
|
|
|
|
var asset Asset
|
|
if err := json.Unmarshal(assetBytes, &asset); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal asset %s: %w", id, err)
|
|
}
|
|
|
|
return &asset, nil
|
|
}
|
|
|
|
func parseCreateInput(assetJSON string) (*AssetCreateInput, error) {
|
|
var input AssetCreateInput
|
|
if err := json.Unmarshal([]byte(assetJSON), &input); err != nil {
|
|
return nil, fmt.Errorf("failed to parse asset JSON: %w", err)
|
|
}
|
|
|
|
if input.ID == "" {
|
|
return nil, fmt.Errorf("missing ID")
|
|
}
|
|
|
|
return &input, nil
|
|
}
|
|
|
|
func parseUpdateInput(assetJSON string) (*AssetUpdateInput, error) {
|
|
var input AssetUpdateInput
|
|
if err := json.Unmarshal([]byte(assetJSON), &input); err != nil {
|
|
return nil, fmt.Errorf("failed to parse asset JSON: %w", err)
|
|
}
|
|
|
|
if input.ID == "" {
|
|
return nil, fmt.Errorf("no asset ID specified")
|
|
}
|
|
|
|
return &input, nil
|
|
}
|
|
|
|
func valueOrDefault(value *int, def int) int {
|
|
if value == nil {
|
|
return def
|
|
}
|
|
|
|
return *value
|
|
}
|
|
|
|
func hasWritePermission(ctx contractapi.TransactionContextInterface, asset *Asset) error {
|
|
clientOrg, err := getClientOrg(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var owner OwnerIdentifier
|
|
if err := json.Unmarshal([]byte(asset.Owner), &owner); err != nil {
|
|
return fmt.Errorf("invalid owner identity: %w", err)
|
|
}
|
|
|
|
if clientOrg != owner.Org {
|
|
return fmt.Errorf("only owner can update assets")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
return ctx.GetClientIdentity().GetMSPID()
|
|
}
|
|
|
|
func clientIdentifier(ctx contractapi.TransactionContextInterface, user *string) (OwnerIdentifier, error) {
|
|
clientOrg, err := getClientOrg(ctx)
|
|
if err != nil {
|
|
return OwnerIdentifier{}, err
|
|
}
|
|
|
|
userName := ""
|
|
if user != nil {
|
|
userName = *user
|
|
} else {
|
|
userName, err = clientCommonName(ctx)
|
|
if err != nil {
|
|
return OwnerIdentifier{}, err
|
|
}
|
|
}
|
|
|
|
return OwnerIdentifier{
|
|
Org: clientOrg,
|
|
User: userName,
|
|
}, nil
|
|
}
|
|
|
|
func clientCommonName(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
cert, err := ctx.GetClientIdentity().GetX509Certificate()
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to get client certificate: %w", err)
|
|
}
|
|
|
|
if cert.Subject.CommonName == "" {
|
|
return "", fmt.Errorf("unable to identify client identity common name")
|
|
}
|
|
|
|
return cert.Subject.CommonName, nil
|
|
}
|
|
|
|
func marshalOwner(owner OwnerIdentifier) string {
|
|
ownerBytes, _ := json.Marshal(owner)
|
|
return string(ownerBytes)
|
|
}
|
|
|
|
func ownerIdentifier(user string, org string) OwnerIdentifier {
|
|
return OwnerIdentifier{Org: org, User: user}
|
|
}
|
|
|
|
func setEndorsingOrgs(ctx contractapi.TransactionContextInterface, ledgerKey string, orgs ...string) error {
|
|
policy, err := statebased.NewStateEP(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = policy.AddOrgs(statebased.RoleTypePeer, orgs...); err != nil {
|
|
return err
|
|
}
|
|
|
|
policyBytes, err := policy.Policy()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.GetStub().SetStateValidationParameter(ledgerKey, policyBytes)
|
|
}
|