diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/Dockerfile b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/Dockerfile new file mode 100644 index 0000000000..272d6ed380 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/Dockerfile @@ -0,0 +1,56 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +#Stage 1 – Builder image +FROM golang:1.23-alpine AS builder + +RUN apk add --no-cache git + +WORKDIR /build + + +COPY go.mod go.sum ./ +RUN go mod download + +COPY src/ ./src/ +RUN CGO_ENABLED=0 go build -v -o /build/chaincode ./src/... + + +# Stage 2 – Chaincode-as-a-Service (CaaS) image +FROM alpine:3.20 AS ccaas + +ARG TARGETARCH +ARG CC_SERVER_PORT=9999 + +# tini gives us proper PID-1 signal handling +ENV TINI_VERSION=v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TARGETARCH} /tini +RUN chmod +x /tini + +RUN addgroup -S chaincode && adduser -S chaincode -G chaincode +WORKDIR /home/chaincode + +COPY --from=builder /build/chaincode ./chaincode +COPY docker/docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh + +ENV PORT=${CC_SERVER_PORT} +EXPOSE ${CC_SERVER_PORT} + +USER chaincode +ENTRYPOINT ["/tini", "--", "./docker-entrypoint.sh"] + + + +# Stage 3 – k8s builder image +FROM alpine:3.20 AS k8s + +RUN addgroup -S chaincode && adduser -S chaincode -G chaincode +WORKDIR /home/chaincode + +COPY --from=builder /build/chaincode ./chaincode +COPY docker/docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x ./docker-entrypoint.sh + +USER chaincode +CMD ["./docker-entrypoint.sh"] diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/asset-transfer-chaincode-vars.yml b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/asset-transfer-chaincode-vars.yml new file mode 100644 index 0000000000..1c83caeae9 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/asset-transfer-chaincode-vars.yml @@ -0,0 +1,11 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +smart_contract_name: "asset-transfer" +smart_contract_version: "1.0.0" +smart_contract_sequence: 1 +smart_contract_package: "asset-transfer.tgz" +# smart_contract_constructor: "" +smart_contract_endorsement_policy: "" +smart_contract_collections_file: "" diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/docker/docker-entrypoint.sh b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/docker/docker-entrypoint.sh new file mode 100644 index 0000000000..3994d15fbf --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/docker/docker-entrypoint.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${CORE_PEER_TLS_ENABLED:=false}" + +if [[ ! -v CHAINCODE_SERVER_ADDRESS ]]; then + # Legacy peer-managed mode: binary acts as a regular chaincode process. + exec ./chaincode --peer.address "${CORE_PEER_ADDRESS}" + +elif [[ "${CORE_PEER_TLS_ENABLED,,}" == "true" ]]; then + # CaaS + TLS: fabric-chaincode-go/v2 reads CHAINCODE_SERVER_ADDRESS, + # CORE_CHAINCODE_ID_NAME, and TLS vars directly as env vars. + exec env \ + CORE_CHAINCODE_ID_NAME="${CHAINCODE_ID}" \ + CHAINCODE_SERVER_ADDRESS="${CHAINCODE_SERVER_ADDRESS}" \ + CORE_PEER_TLS_ENABLED=true \ + CORE_PEER_TLS_ROOTCERT_FILE="${CHAINCODE_TLS_KEY:-/hyperledger/privatekey.pem}" \ + CORE_TLS_CLIENT_KEY_FILE="${CHAINCODE_TLS_CERT:-/hyperledger/cert.pem}" \ + CORE_TLS_CLIENT_CERT_FILE="${CHAINCODE_TLS_CLIENT_CACERT:-/hyperledger/rootcert.pem}" \ + ./chaincode + +else + # CaaS without TLS: fabric-chaincode-go/v2 uses CHAINCODE_SERVER_ADDRESS + # and CORE_CHAINCODE_ID_NAME env vars to start the gRPC server. + exec env \ + CORE_CHAINCODE_ID_NAME="${CHAINCODE_ID}" \ + CHAINCODE_SERVER_ADDRESS="${CHAINCODE_SERVER_ADDRESS}" \ + ./chaincode +fi diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.mod b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.mod new file mode 100644 index 0000000000..ad08e38434 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.mod @@ -0,0 +1,30 @@ +module asset + +go 1.23.0 + +require ( + github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0 + github.com/hyperledger/fabric-contract-api-go/v2 v2.2.0 + github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 + google.golang.org/protobuf v1.36.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.sum b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.sum new file mode 100644 index 0000000000..1459ca55c9 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/go.sum @@ -0,0 +1,61 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0 h1:IhkHfrl5X/fVnmB6pWeCYCdIJRi9bxj+WTnVN8DtW3c= +github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0/go.mod h1:PHHaFffjw7p7n9bmCfcm7RqDqYdivNEsJdiNIKZo5Lk= +github.com/hyperledger/fabric-contract-api-go/v2 v2.2.0 h1:rmUoBmciB0GL/miqcbJmJbgp5QTWoJUrZo+CNxrNLF4= +github.com/hyperledger/fabric-contract-api-go/v2 v2.2.0/go.mod h1:FeWeO/jwGjiME7ak3GufqKIcwkejtzrDG4QxbfKydWs= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 h1:YJrd+gMaeY0/vsN0aS0QkEKTivGoUnSRIXxGJ7KI+Pc= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4/go.mod h1:bau/6AJhvEcu9GKKYHlDXAxXKzYNfhP6xu2GXuxEcFk= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset.go b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset.go new file mode 100644 index 0000000000..d27f2ad26f --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset.go @@ -0,0 +1,14 @@ +package main + +type OwnerIdentifier struct { + Org string `json:"org"` + User string `json:"user"` +} + +type Asset struct { + AppraisedValue int `json:"AppraisedValue"` + Color string `json:"Color"` + ID string `json:"ID"` + Owner string `json:"Owner"` // JSON-encoded OwnerIdentifier + Size int `json:"Size"` +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset_transfer.go b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset_transfer.go new file mode 100644 index 0000000000..823715ca51 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/asset_transfer.go @@ -0,0 +1,307 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/hyperledger/fabric-chaincode-go/v2/pkg/cid" + "github.com/hyperledger/fabric-chaincode-go/v2/pkg/statebased" + "github.com/hyperledger/fabric-contract-api-go/v2/contractapi" +) + +// SmartContract provides functions for managing assets. +type SmartContract struct { + contractapi.Contract +} + +func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { + exists, err := s.AssetExists(ctx, id) + if err != nil { + return err + } + if exists { + return fmt.Errorf("The asset %s already exists", id) + } + + ownerID, err := clientIdentifier(ctx, owner) + if err != nil { + return err + } + ownerJSON, err := json.Marshal(ownerID) + if err != nil { + return err + } + + asset := Asset{ + AppraisedValue: appraisedValue, + Color: color, + ID: id, + Owner: string(ownerJSON), + Size: size, + } + assetBytes, err := json.Marshal(asset) + if err != nil { + return err + } + + if err := ctx.GetStub().PutState(id, assetBytes); err != nil { + return err + } + + mspID, err := cid.GetMSPID(ctx.GetStub()) + if err != nil { + return err + } + if err := setEndorsingOrgs(ctx, id, mspID); err != nil { + return err + } + + return ctx.GetStub().SetEvent("CreateAsset", assetBytes) +} + +func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { + assetBytes, err := readAsset(ctx, id) + if err != nil { + return nil, err + } + + var asset Asset + if err := json.Unmarshal(assetBytes, &asset); err != nil { + return nil, err + } + + return &asset, nil +} + +// UpdateAsset updates color, size, and appraised value of an existing asset. +// The asset owner cannot be changed here; use TransferAsset instead. +func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, appraisedValue int) error { + assetBytes, err := readAsset(ctx, id) + if err != nil { + return err + } + + var existing Asset + if err := json.Unmarshal(assetBytes, &existing); err != nil { + return err + } + + ok, err := hasWritePermission(ctx, &existing) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("Only owner can update assets") + } + + // Owner is intentionally preserved; use TransferAsset to change owner. + existing.Color = color + existing.Size = size + existing.AppraisedValue = appraisedValue + + updatedBytes, err := json.Marshal(existing) + if err != nil { + return err + } + + if err := ctx.GetStub().PutState(id, updatedBytes); err != nil { + return err + } + + mspID, err := cid.GetMSPID(ctx.GetStub()) + if err != nil { + return err + } + if err := setEndorsingOrgs(ctx, id, mspID); err != nil { + return err + } + + return ctx.GetStub().SetEvent("UpdateAsset", updatedBytes) +} + +// DeleteAsset deletes an asset from the world state. +func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { + assetBytes, err := readAsset(ctx, id) + if err != nil { + return err + } + + var asset Asset + if err := json.Unmarshal(assetBytes, &asset); err != nil { + return err + } + + ok, err := hasWritePermission(ctx, &asset) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("Only owner can delete assets") + } + + if err := ctx.GetStub().DelState(id); err != nil { + return err + } + + return ctx.GetStub().SetEvent("DeleteAsset", assetBytes) +} + +// AssetExists returns true when an asset with the given ID exists in the world state. +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 from world state: %v", err) + } + + return assetBytes != nil, nil +} + +// TransferAsset updates the owner of an asset with the given ID. +// newOwner is the user identifier; newOwnerOrg is the MSP ID of the new owning organisation. +// Subsequent updates must be endorsed by the new owning organisation. +func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string, newOwnerOrg string) error { + assetBytes, err := readAsset(ctx, id) + if err != nil { + return err + } + + var asset Asset + if err := json.Unmarshal(assetBytes, &asset); err != nil { + return err + } + + ok, err := hasWritePermission(ctx, &asset) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("Only owner can transfer assets") + } + + newOwnerID := OwnerIdentifier{Org: newOwnerOrg, User: newOwner} + ownerJSON, err := json.Marshal(newOwnerID) + if err != nil { + return err + } + asset.Owner = string(ownerJSON) + + updatedBytes, err := json.Marshal(asset) + if err != nil { + return err + } + + if err := ctx.GetStub().PutState(id, updatedBytes); err != nil { + return err + } + + if err := setEndorsingOrgs(ctx, id, newOwnerOrg); err != nil { + return err + } + + return ctx.GetStub().SetEvent("TransferAsset", updatedBytes) +} + +// GetAllAssets returns all assets found in the world state. +func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) { + // Range query with empty start/end key returns all assets in the chaincode namespace. + 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 { + log.Printf("skipping malformed asset entry: %v", err) + continue + } + assets = append(assets, &asset) + } + + return assets, nil +} + +// --- internal helpers ----------------------- + +func readAsset(ctx contractapi.TransactionContextInterface, id string) ([]byte, error) { + assetBytes, err := ctx.GetStub().GetState(id) + if err != nil { + return nil, fmt.Errorf("failed to read from world state: %v", err) + } + if assetBytes == nil { + return nil, fmt.Errorf("Sorry, asset %s has not been created", id) + } + + return assetBytes, nil +} + +func hasWritePermission(ctx contractapi.TransactionContextInterface, asset *Asset) (bool, error) { + clientID, err := clientIdentifier(ctx, "") + if err != nil { + return false, err + } + + var ownerID OwnerIdentifier + if err := json.Unmarshal([]byte(asset.Owner), &ownerID); err != nil { + return false, fmt.Errorf("failed to parse asset owner: %v", err) + } + + return clientID.Org == ownerID.Org, nil +} + +func clientIdentifier(ctx contractapi.TransactionContextInterface, user string) (OwnerIdentifier, error) { + mspID, err := cid.GetMSPID(ctx.GetStub()) + if err != nil { + return OwnerIdentifier{}, err + } + + if user == "" { + cn, err := clientCommonName(ctx) + if err != nil { + return OwnerIdentifier{}, err + } + user = cn + } + + return OwnerIdentifier{Org: mspID, User: user}, nil +} + +func clientCommonName(ctx contractapi.TransactionContextInterface) (string, error) { + cert, err := cid.GetX509Certificate(ctx.GetStub()) + if err != nil { + return "", err + } + if cert.Subject.CommonName == "" { + return "", fmt.Errorf("unable to identify client identity common name") + } + + return cert.Subject.CommonName, nil +} + +func setEndorsingOrgs(ctx contractapi.TransactionContextInterface, key string, orgs ...string) error { + ep, err := statebased.NewStateEP(nil) + if err != nil { + return err + } + if err := ep.AddOrgs(statebased.RoleTypeMember, orgs...); err != nil { + return err + } + policy, err := ep.Policy() + if err != nil { + return err + } + + return ctx.GetStub().SetStateValidationParameter(key, policy) +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/main.go b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/main.go new file mode 100644 index 0000000000..44f3416a70 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-go/src/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + + "github.com/hyperledger/fabric-contract-api-go/v2/contractapi" +) + +func main() { + assetChaincode, err := contractapi.NewChaincode(&SmartContract{}) + if err != nil { + log.Panicf("Error creating asset-transfer chaincode: %v", err) + } + + if err := assetChaincode.Start(); err != nil { + log.Panicf("Error starting asset-transfer chaincode: %v", err) + } +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json index 5ca122622a..e8c1854942 100644 --- a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json @@ -57,6 +57,7 @@ "@types/node": "^20.19.33", "eslint": "^10.0.2", "typescript": "~5.8", - "typescript-eslint": "^8.56.1" + "typescript-eslint": "^8.56.1", + "vitest": "^4.0.18" } }