Skip to content

Commit e860de9

Browse files
committed
Add Go smart contract implementation for full-stack-asset-transfer-guide
1 parent a2c40e6 commit e860de9

6 files changed

Lines changed: 488 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
---
5+
smart_contract_name: "asset-transfer"
6+
smart_contract_version: "1.0.0"
7+
smart_contract_sequence: 1
8+
smart_contract_package: "asset-transfer.tgz"
9+
# smart_contract_constructor: "initLedger"
10+
smart_contract_endorsement_policy: ""
11+
smart_contract_collections_file: ""
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/hyperledger/fabric-chaincode-go/v2/pkg/statebased"
8+
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
9+
)
10+
11+
type SmartContract struct {
12+
contractapi.Contract
13+
}
14+
15+
type Asset struct {
16+
AppraisedValue int `json:"AppraisedValue"`
17+
Color string `json:"Color"`
18+
ID string `json:"ID"`
19+
Owner string `json:"Owner"`
20+
Size int `json:"Size"`
21+
}
22+
23+
type OwnerIdentifier struct {
24+
Org string `json:"org"`
25+
User string `json:"user"`
26+
}
27+
28+
type AssetCreateInput struct {
29+
ID string `json:"ID"`
30+
Color string `json:"Color,omitempty"`
31+
Owner *string `json:"Owner,omitempty"`
32+
AppraisedValue *int `json:"AppraisedValue,omitempty"`
33+
Size *int `json:"Size,omitempty"`
34+
}
35+
36+
type AssetUpdateInput struct {
37+
ID string `json:"ID"`
38+
Color *string `json:"Color,omitempty"`
39+
AppraisedValue *int `json:"AppraisedValue,omitempty"`
40+
Size *int `json:"Size,omitempty"`
41+
Owner *string `json:"Owner,omitempty"`
42+
}
43+
44+
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, assetJSON string) error {
45+
input, err := parseCreateInput(assetJSON)
46+
if err != nil {
47+
return err
48+
}
49+
50+
exists, err := s.AssetExists(ctx, input.ID)
51+
if err != nil {
52+
return err
53+
}
54+
if exists {
55+
return fmt.Errorf("the asset %s already exists", input.ID)
56+
}
57+
58+
owner, err := clientIdentifier(ctx, input.Owner)
59+
if err != nil {
60+
return err
61+
}
62+
63+
asset := Asset{
64+
ID: input.ID,
65+
Color: input.Color,
66+
Size: valueOrDefault(input.Size, 0),
67+
Owner: marshalOwner(owner),
68+
AppraisedValue: valueOrDefault(input.AppraisedValue, 0),
69+
}
70+
71+
assetBytes, err := json.Marshal(asset)
72+
if err != nil {
73+
return err
74+
}
75+
76+
if err := ctx.GetStub().PutState(asset.ID, assetBytes); err != nil {
77+
return fmt.Errorf("failed to put asset in world state: %w", err)
78+
}
79+
80+
if err := setEndorsingOrgs(ctx, asset.ID, owner.Org); err != nil {
81+
return err
82+
}
83+
84+
return ctx.GetStub().SetEvent("CreateAsset", assetBytes)
85+
}
86+
87+
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
88+
asset, err := s.readAsset(ctx, id)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
return asset, nil
94+
}
95+
96+
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, assetJSON string) error {
97+
input, err := parseUpdateInput(assetJSON)
98+
if err != nil {
99+
return err
100+
}
101+
102+
existingAsset, err := s.readAsset(ctx, input.ID)
103+
if err != nil {
104+
return err
105+
}
106+
107+
if err := hasWritePermission(ctx, existingAsset); err != nil {
108+
return err
109+
}
110+
111+
updatedAsset := *existingAsset
112+
if input.Color != nil {
113+
updatedAsset.Color = *input.Color
114+
}
115+
if input.Size != nil {
116+
updatedAsset.Size = *input.Size
117+
}
118+
if input.AppraisedValue != nil {
119+
updatedAsset.AppraisedValue = *input.AppraisedValue
120+
}
121+
// Owner cannot be updated via UpdateAsset. TransferAsset must be used for ownership changes.
122+
123+
assetBytes, err := json.Marshal(updatedAsset)
124+
if err != nil {
125+
return err
126+
}
127+
128+
if err := ctx.GetStub().PutState(updatedAsset.ID, assetBytes); err != nil {
129+
return fmt.Errorf("failed to put updated asset in world state: %w", err)
130+
}
131+
132+
clientOrg, err := getClientOrg(ctx)
133+
if err != nil {
134+
return err
135+
}
136+
if err := setEndorsingOrgs(ctx, updatedAsset.ID, clientOrg); err != nil {
137+
return err
138+
}
139+
140+
if err := ctx.GetStub().SetEvent("UpdateAsset", assetBytes); err != nil {
141+
return err
142+
}
143+
144+
return nil
145+
}
146+
147+
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
148+
asset, err := s.readAsset(ctx, id)
149+
if err != nil {
150+
return err
151+
}
152+
153+
if err := hasWritePermission(ctx, asset); err != nil {
154+
return err
155+
}
156+
157+
if err := ctx.GetStub().DelState(id); err != nil {
158+
return fmt.Errorf("failed to delete asset %s: %w", id, err)
159+
}
160+
161+
return ctx.GetStub().SetEvent("DeleteAsset", []byte(id))
162+
}
163+
164+
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
165+
assetBytes, err := ctx.GetStub().GetState(id)
166+
if err != nil {
167+
return false, fmt.Errorf("failed to read world state: %w", err)
168+
}
169+
170+
return len(assetBytes) > 0, nil
171+
}
172+
173+
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string, newOwnerOrg string) error {
174+
asset, err := s.readAsset(ctx, id)
175+
if err != nil {
176+
return err
177+
}
178+
179+
if err := hasWritePermission(ctx, asset); err != nil {
180+
return err
181+
}
182+
183+
owner := ownerIdentifier(newOwner, newOwnerOrg)
184+
ownerJSON, err := json.Marshal(owner)
185+
if err != nil {
186+
return err
187+
}
188+
asset.Owner = string(ownerJSON)
189+
190+
assetBytes, err := json.Marshal(asset)
191+
if err != nil {
192+
return err
193+
}
194+
195+
if err := ctx.GetStub().PutState(id, assetBytes); err != nil {
196+
return fmt.Errorf("failed to update asset owner in world state: %w", err)
197+
}
198+
199+
if err := setEndorsingOrgs(ctx, id, newOwnerOrg); err != nil {
200+
return err
201+
}
202+
203+
return ctx.GetStub().SetEvent("TransferAsset", assetBytes)
204+
}
205+
206+
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]Asset, error) {
207+
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
208+
if err != nil {
209+
return nil, err
210+
}
211+
defer resultsIterator.Close()
212+
213+
var assets []Asset
214+
for resultsIterator.HasNext() {
215+
queryResponse, err := resultsIterator.Next()
216+
if err != nil {
217+
return nil, err
218+
}
219+
220+
var asset Asset
221+
if err := json.Unmarshal(queryResponse.Value, &asset); err != nil {
222+
continue
223+
}
224+
assets = append(assets, asset)
225+
}
226+
227+
return assets, nil
228+
}
229+
230+
func (s *SmartContract) readAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
231+
assetBytes, err := ctx.GetStub().GetState(id)
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to read world state: %w", err)
234+
}
235+
if assetBytes == nil || len(assetBytes) == 0 {
236+
return nil, fmt.Errorf("sorry, asset %s has not been created", id)
237+
}
238+
239+
var asset Asset
240+
if err := json.Unmarshal(assetBytes, &asset); err != nil {
241+
return nil, fmt.Errorf("failed to unmarshal asset %s: %w", id, err)
242+
}
243+
244+
return &asset, nil
245+
}
246+
247+
func parseCreateInput(assetJSON string) (*AssetCreateInput, error) {
248+
var input AssetCreateInput
249+
if err := json.Unmarshal([]byte(assetJSON), &input); err != nil {
250+
return nil, fmt.Errorf("failed to parse asset JSON: %w", err)
251+
}
252+
253+
if input.ID == "" {
254+
return nil, fmt.Errorf("missing ID")
255+
}
256+
257+
return &input, nil
258+
}
259+
260+
func parseUpdateInput(assetJSON string) (*AssetUpdateInput, error) {
261+
var input AssetUpdateInput
262+
if err := json.Unmarshal([]byte(assetJSON), &input); err != nil {
263+
return nil, fmt.Errorf("failed to parse asset JSON: %w", err)
264+
}
265+
266+
if input.ID == "" {
267+
return nil, fmt.Errorf("no asset ID specified")
268+
}
269+
270+
return &input, nil
271+
}
272+
273+
func valueOrDefault(value *int, def int) int {
274+
if value == nil {
275+
return def
276+
}
277+
278+
return *value
279+
}
280+
281+
func hasWritePermission(ctx contractapi.TransactionContextInterface, asset *Asset) error {
282+
clientOrg, err := getClientOrg(ctx)
283+
if err != nil {
284+
return err
285+
}
286+
287+
var owner OwnerIdentifier
288+
if err := json.Unmarshal([]byte(asset.Owner), &owner); err != nil {
289+
return fmt.Errorf("invalid owner identity: %w", err)
290+
}
291+
292+
if clientOrg != owner.Org {
293+
return fmt.Errorf("only owner can update assets")
294+
}
295+
296+
return nil
297+
}
298+
299+
func getClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
300+
return ctx.GetClientIdentity().GetMSPID()
301+
}
302+
303+
func clientIdentifier(ctx contractapi.TransactionContextInterface, user *string) (OwnerIdentifier, error) {
304+
clientOrg, err := getClientOrg(ctx)
305+
if err != nil {
306+
return OwnerIdentifier{}, err
307+
}
308+
309+
userName := ""
310+
if user != nil {
311+
userName = *user
312+
} else {
313+
userName, err = clientCommonName(ctx)
314+
if err != nil {
315+
return OwnerIdentifier{}, err
316+
}
317+
}
318+
319+
return OwnerIdentifier{
320+
Org: clientOrg,
321+
User: userName,
322+
}, nil
323+
}
324+
325+
func clientCommonName(ctx contractapi.TransactionContextInterface) (string, error) {
326+
cert, err := ctx.GetClientIdentity().GetX509Certificate()
327+
if err != nil {
328+
return "", fmt.Errorf("unable to get client certificate: %w", err)
329+
}
330+
331+
if cert.Subject.CommonName == "" {
332+
return "", fmt.Errorf("unable to identify client identity common name")
333+
}
334+
335+
return cert.Subject.CommonName, nil
336+
}
337+
338+
func marshalOwner(owner OwnerIdentifier) string {
339+
ownerBytes, _ := json.Marshal(owner)
340+
return string(ownerBytes)
341+
}
342+
343+
func ownerIdentifier(user string, org string) OwnerIdentifier {
344+
return OwnerIdentifier{Org: org, User: user}
345+
}
346+
347+
func setEndorsingOrgs(ctx contractapi.TransactionContextInterface, ledgerKey string, orgs ...string) error {
348+
policy, err := statebased.NewStateEP(nil)
349+
if err != nil {
350+
return err
351+
}
352+
353+
if err = policy.AddOrgs(statebased.RoleTypePeer, orgs...); err != nil {
354+
return err
355+
}
356+
357+
policyBytes, err := policy.Policy()
358+
if err != nil {
359+
return err
360+
}
361+
362+
return ctx.GetStub().SetStateValidationParameter(ledgerKey, policyBytes)
363+
}

0 commit comments

Comments
 (0)