diff --git a/full-stack-asset-transfer-guide/applications/trader-go/commands.go b/full-stack-asset-transfer-guide/applications/trader-go/commands.go new file mode 100644 index 0000000000..6615760913 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/commands.go @@ -0,0 +1,15 @@ +package main + +import "github.com/hyperledger/fabric-gateway/pkg/client" + +type Command func(gw *client.Gateway, args []string) error + +var commands = map[string]Command{ + "create": cmdCreate, + "delete": cmdDelete, + "getAllAssets": cmdGetAllAssets, + "listen": cmdListen, + "read": cmdRead, + "transact": cmdTransact, + "transfer": cmdTransfer, +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/commands_impl.go b/full-stack-asset-transfer-guide/applications/trader-go/commands_impl.go new file mode 100644 index 0000000000..aae27b28cb --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/commands_impl.go @@ -0,0 +1,296 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "os" + "strconv" + "strings" + "sync" + + "github.com/hyperledger/fabric-gateway/pkg/client" +) + +// cmdCreate creates a new asset on the ledger. +// Arguments: +func cmdCreate(gw *client.Gateway, args []string) error { + if len(args) < 3 { + return fmt.Errorf("arguments: ") + } + + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + return smartContract.CreateAsset(Asset{ + ID: args[0], + Owner: args[1], + Color: args[2], + Size: 1, + AppraisedValue: 1, + }) +} + +// cmdDelete deletes an asset from the ledger. +// Arguments: +func cmdDelete(gw *client.Gateway, args []string) error { + if len(args) < 1 { + return fmt.Errorf("arguments: ") + } + + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + return smartContract.DeleteAsset(args[0]) +} + +// cmdGetAllAssets queries and prints all assets currently on the ledger. +func cmdGetAllAssets(gw *client.Gateway, _ []string) error { + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + assets, err := smartContract.GetAllAssets() + if err != nil { + return err + } + + data, err := json.MarshalIndent(assets, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal assets: %w", err) + } + + for _, line := range strings.Split(string(data), "\n") { + fmt.Println(line) + } + return nil +} + +const startBlock = uint64(0) + +func cmdListen(gw *client.Gateway, _ []string) error { + network := gw.GetNetwork(channelName()) + + checkpointFile := os.Getenv("CHECKPOINT_FILE") + if checkpointFile == "" { + checkpointFile = "checkpoint.json" + } + + simulatedFailureCount, err := getSimulatedFailureCount() + if err != nil { + return err + } + + checkpointer, err := client.NewFileCheckpointer(checkpointFile) + if err != nil { + return fmt.Errorf("failed to create checkpointer: %w", err) + } + defer checkpointer.Close() + + displayBlock := checkpointer.BlockNumber() + if displayBlock == 0 { + displayBlock = startBlock + } + + fmt.Println("Starting event listening from block", displayBlock) + fmt.Println("Last processed transaction ID within block:", checkpointer.TransactionID()) + if simulatedFailureCount > 0 { + fmt.Println("Simulating a write failure every", simulatedFailureCount, "transactions") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + events, err := network.ChaincodeEvents(ctx, chaincodeName(), + client.WithStartBlock(startBlock), + client.WithCheckpoint(checkpointer), + ) + if err != nil { + return fmt.Errorf("failed to start chaincode event subscription: %w", err) + } + + eventCount := 0 + for event := range events { + if simulatedFailureCount > 0 { + eventCount++ + if eventCount >= simulatedFailureCount { + eventCount = 0 + return &ExpectedError{Message: "Simulated write failure"} + } + } + fmt.Printf("Chaincode event: BlockNumber=%d TxID=%s Name=%s Payload=%s\n", + event.BlockNumber, event.TransactionID, event.EventName, string(event.Payload)) + } + + return nil +} + +func getSimulatedFailureCount() (int, error) { + value := os.Getenv("SIMULATED_FAILURE_COUNT") + if value == "" { + return 0, nil + } + count, err := strconv.Atoi(value) + if err != nil || count < 0 { + return 0, fmt.Errorf("invalid SIMULATED_FAILURE_COUNT value: %s", value) + } + return count, nil +} + +// cmdRead reads and prints a single asset from the ledger. +// Arguments: +func cmdRead(gw *client.Gateway, args []string) error { + if len(args) < 1 { + return fmt.Errorf("arguments: ") + } + + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + asset, err := smartContract.ReadAsset(args[0]) + if err != nil { + return err + } + + data, err := json.MarshalIndent(asset, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal asset: %w", err) + } + + fmt.Println(string(data)) + return nil +} + +var ( + colors = []string{"red", "green", "blue"} + maxInitialSize = 10 + maxInitialVal = 1000 +) + +// cmdTransact runs a batch of concurrent create/update/delete transactions to demonstrate +func cmdTransact(gw *client.Gateway, _ []string) error { + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + app := &transactApp{smartContract: smartContract, batchSize: 6} + return app.run() +} + +type transactApp struct { + smartContract *AssetTransfer + batchSize int +} + +func (a *transactApp) run() error { + var wg sync.WaitGroup + errCh := make(chan error, a.batchSize) + + for i := 0; i < a.batchSize; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if err := a.transact(); err != nil { + errCh <- err + } + }() + } + + wg.Wait() + close(errCh) + + var failures []string + for err := range errCh { + failures = append(failures, err.Error()) + } + if len(failures) > 0 { + return fmt.Errorf("%d failures:\n- %s", len(failures), strings.Join(failures, "\n- ")) + } + return nil +} + +func (a *transactApp) transact() error { + asset := a.newAsset() + + if err := a.smartContract.CreateAsset(asset); err != nil { + return err + } + fmt.Printf("Created asset %s\n", asset.ID) + + if randomInt(2) == 0 { + oldColor := asset.Color + asset.Color = differentElement(colors, oldColor) + if err := a.smartContract.UpdateAsset(asset); err != nil { + return err + } + fmt.Printf("Updated color of asset %s from %s to %s\n", asset.ID, oldColor, asset.Color) + } + + if randomInt(4) == 0 { + if err := a.smartContract.DeleteAsset(asset.ID); err != nil { + return err + } + fmt.Printf("Deleted asset %s\n", asset.ID) + } + + return nil +} + +func (a *transactApp) newAsset() Asset { + return Asset{ + ID: randomHexString(8), + Color: randomElement(colors), + Size: randomInt(maxInitialSize) + 1, + AppraisedValue: float64(randomInt(maxInitialVal) + 1), + } +} + +func randomHexString(length int) string { + b := make([]byte, (length+1)/2) + if _, err := rand.Read(b); err != nil { + panic(fmt.Sprintf("failed to generate random bytes: %v", err)) + } + return hex.EncodeToString(b)[:length] +} + +func randomInt(max int) int { + n, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + panic(fmt.Sprintf("failed to generate random int: %v", err)) + } + return int(n.Int64()) +} + +func randomElement(values []string) string { + return values[randomInt(len(values))] +} + +func differentElement(values []string, currentValue string) string { + var candidates []string + for _, v := range values { + if v != currentValue { + candidates = append(candidates, v) + } + } + return randomElement(candidates) +} + +// cmdTransfer transfers ownership of an asset to a new owner in a different organisation. +// Arguments: +func cmdTransfer(gw *client.Gateway, args []string) error { + if len(args) < 3 { + return fmt.Errorf("arguments: ") + } + + network := gw.GetNetwork(channelName()) + contract := network.GetContract(chaincodeName()) + + smartContract := NewAssetTransfer(contract) + return smartContract.TransferAsset(args[0], args[1], args[2]) +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/config.go b/full-stack-asset-transfer-guide/applications/trader-go/config.go new file mode 100644 index 0000000000..1cee1d9ad8 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/config.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os" +) + +func gatewayEndpoint() (string, error) { + return requireEnv("ENDPOINT") +} + +func mspID() (string, error) { + return requireEnv("MSP_ID") +} + +func clientCertPath() (string, error) { + return requireEnv("CERTIFICATE") +} + +func privateKeyPath() (string, error) { + return requireEnv("PRIVATE_KEY") +} + +func tlsCertPath() string { + return os.Getenv("TLS_CERT") +} + +func channelName() string { + if v := os.Getenv("CHANNEL_NAME"); v != "" { + return v + } + return "mychannel" +} + +func chaincodeName() string { + if v := os.Getenv("CHAINCODE_NAME"); v != "" { + return v + } + return "asset-transfer" +} + +func hostAlias() string { + return os.Getenv("HOST_ALIAS") +} + +func requireEnv(name string) (string, error) { + v := os.Getenv(name) + if v == "" { + printEnvUsage() + return "", fmt.Errorf("environment variable %s not set", name) + } + return v, nil +} + +func printEnvUsage() { + fmt.Fprintln(os.Stderr, "The following environment variables must be set:") + fmt.Fprintln(os.Stderr, " ENDPOINT - Endpoint address of the gateway service") + fmt.Fprintln(os.Stderr, " MSP_ID - User's organization Member Services Provider ID") + fmt.Fprintln(os.Stderr, " CERTIFICATE - User's certificate file") + fmt.Fprintln(os.Stderr, " PRIVATE_KEY - User's private key file") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "The following environment variables are optional:") + fmt.Fprintln(os.Stderr, " CHANNEL_NAME - Channel to which the chaincode is deployed") + fmt.Fprintln(os.Stderr, " CHAINCODE_NAME - Chaincode deployed to the channel") + fmt.Fprintln(os.Stderr, " TLS_CERT - TLS CA root certificate (only if using TLS and private CA)") + fmt.Fprintln(os.Stderr, " HOST_ALIAS - TLS hostname override (only if TLS cert does not match endpoint)") +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/connect.go b/full-stack-asset-transfer-guide/applications/trader-go/connect.go new file mode 100644 index 0000000000..f379adfaae --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/connect.go @@ -0,0 +1,122 @@ +package main + +import ( + "crypto/x509" + "fmt" + "os" + "time" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-gateway/pkg/hash" + "github.com/hyperledger/fabric-gateway/pkg/identity" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// newGrpcConnection creates a gRPC connection to the Gateway server. +// If TLS_CERT is set, TLS is used; otherwise an insecure connection is established. +func newGrpcConnection() (*grpc.ClientConn, error) { + endpoint, err := gatewayEndpoint() + if err != nil { + return nil, err + } + + tlsCert := tlsCertPath() + if tlsCert != "" { + certPEM, err := os.ReadFile(tlsCert) + if err != nil { + return nil, fmt.Errorf("failed to read TLS certificate: %w", err) + } + + cert, err := identity.CertificateFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to parse TLS certificate: %w", err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(cert) + + transportCreds := credentials.NewClientTLSFromCert(certPool, "") + opts := []grpc.DialOption{grpc.WithTransportCredentials(transportCreds)} + + // Override TLS server name if endpoint address doesn't match the certificate + if alias := hostAlias(); alias != "" { + opts = append(opts, grpc.WithAuthority(alias)) + } + + return grpc.NewClient(endpoint, opts...) + } + + return grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) +} + +// newGatewayConnection creates a Fabric Gateway connection using the provided gRPC connection. +func newGatewayConnection(grpcConn *grpc.ClientConn) (*client.Gateway, error) { + id, err := newIdentity() + if err != nil { + return nil, err + } + + sign, err := newSigner() + if err != nil { + return nil, err + } + + return client.Connect( + id, + client.WithSign(sign), + client.WithHash(hash.SHA256), + client.WithClientConnection(grpcConn), + // Default timeouts for different gRPC calls + client.WithEvaluateTimeout(5*time.Second), + client.WithEndorseTimeout(15*time.Second), + client.WithSubmitTimeout(5*time.Second), + client.WithCommitStatusTimeout(1*time.Minute), + ) +} + +// newIdentity creates a client X.509 identity from the certificate file. +func newIdentity() (*identity.X509Identity, error) { + certPath, err := clientCertPath() + if err != nil { + return nil, err + } + + certPEM, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("failed to read certificate: %w", err) + } + + cert, err := identity.CertificateFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + msp, err := mspID() + if err != nil { + return nil, err + } + + return identity.NewX509Identity(msp, cert) +} + +// newSigner creates a signing function from the private key file. +func newSigner() (identity.Sign, error) { + keyPath, err := privateKeyPath() + if err != nil { + return nil, err + } + + keyPEM, err := os.ReadFile(keyPath) + if err != nil { + return nil, fmt.Errorf("failed to read private key: %w", err) + } + + privateKey, err := identity.PrivateKeyFromPEM(keyPEM) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + return identity.NewPrivateKeySign(privateKey) +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/contract.go b/full-stack-asset-transfer-guide/applications/trader-go/contract.go new file mode 100644 index 0000000000..5858757a1c --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/contract.go @@ -0,0 +1,121 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" +) + +const retries = 2 + +type Asset struct { + ID string `json:"ID"` + Color string `json:"Color"` + Size int `json:"Size"` + Owner string `json:"Owner"` + AppraisedValue float64 `json:"AppraisedValue"` +} + +type contractCaller interface { + Submit(name string, options ...client.ProposalOption) ([]byte, error) + Evaluate(name string, options ...client.ProposalOption) ([]byte, error) +} + +type AssetTransfer struct { + contract contractCaller +} + +func NewAssetTransfer(contract *client.Contract) *AssetTransfer { + return &AssetTransfer{contract: contract} +} + +func (a *AssetTransfer) CreateAsset(asset Asset) error { + data, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to marshal asset: %w", err) + } + _, err = a.contract.Submit("CreateAsset", client.WithArguments(string(data))) + return err +} + +func (a *AssetTransfer) GetAllAssets() ([]Asset, error) { + result, err := a.contract.Evaluate("GetAllAssets") + if err != nil { + return nil, err + } + if len(result) == 0 { + return []Asset{}, nil + } + + var assets []Asset + if err := json.Unmarshal(result, &assets); err != nil { + return nil, fmt.Errorf("failed to unmarshal assets: %w", err) + } + return assets, nil +} + +func (a *AssetTransfer) ReadAsset(id string) (*Asset, error) { + result, err := a.contract.Evaluate("ReadAsset", client.WithArguments(id)) + if err != nil { + return nil, err + } + + var asset Asset + if err := json.Unmarshal(result, &asset); err != nil { + return nil, fmt.Errorf("failed to unmarshal asset: %w", err) + } + return &asset, nil +} + +func (a *AssetTransfer) UpdateAsset(asset Asset) error { + data, err := json.Marshal(asset) + if err != nil { + return fmt.Errorf("failed to marshal asset: %w", err) + } + return submitWithRetry(func() error { + _, err := a.contract.Submit("UpdateAsset", client.WithArguments(string(data))) + return err + }) +} + +func (a *AssetTransfer) DeleteAsset(id string) error { + return submitWithRetry(func() error { + _, err := a.contract.Submit("DeleteAsset", client.WithArguments(id)) + return err + }) +} + +func (a *AssetTransfer) AssetExists(id string) (bool, error) { + result, err := a.contract.Evaluate("AssetExists", client.WithArguments(id)) + if err != nil { + return false, err + } + return string(result) == "true", nil +} + +func (a *AssetTransfer) TransferAsset(id, newOwner, newOwnerOrg string) error { + fmt.Printf("transferring asset '%s' to %s, %s\n", id, newOwner, newOwnerOrg) + _, err := a.contract.Submit("TransferAsset", client.WithArguments(id, newOwner, newOwnerOrg)) + return err +} + +func submitWithRetry(submit func() error) error { + var lastErr error + for i := 0; i < retries; i++ { + err := submit() + if err == nil { + return nil + } + lastErr = err + + var commitErr *client.CommitError + if errors.As(err, &commitErr) && commitErr.Code == peer.TxValidationCode_MVCC_READ_CONFLICT { + continue + } + break + } + return lastErr +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/expectedError.go b/full-stack-asset-transfer-guide/applications/trader-go/expectedError.go new file mode 100644 index 0000000000..366e65c052 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/expectedError.go @@ -0,0 +1,17 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +// ExpectedError represents a known, expected application error that should be +// displayed normally rather than treated as an unexpected failure. +type ExpectedError struct { + Message string +} + +func (e *ExpectedError) Error() string { + return e.Message +} diff --git a/full-stack-asset-transfer-guide/applications/trader-go/go.mod b/full-stack-asset-transfer-guide/applications/trader-go/go.mod new file mode 100644 index 0000000000..5813381c3e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/go.mod @@ -0,0 +1,18 @@ +module trader-go + +go 1.24.0 + +require ( + github.com/hyperledger/fabric-gateway v1.10.0 + github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7 + google.golang.org/grpc v1.76.0 +) + +require ( + github.com/miekg/pkcs11 v1.1.1 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/full-stack-asset-transfer-guide/applications/trader-go/go.sum b/full-stack-asset-transfer-guide/applications/trader-go/go.sum new file mode 100644 index 0000000000..abb099b639 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/go.sum @@ -0,0 +1,52 @@ +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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hyperledger/fabric-gateway v1.10.0 h1:x5z/pofdVYIqgMo9QWejubfAZYCSt94WdUPj4Wipdeg= +github.com/hyperledger/fabric-gateway v1.10.0/go.mod h1:fSFS1vQkPZq6inNvzsnI/7PCaKSU+UZOZ6uAuau0Yq0= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7 h1:sQ5qv8vQQfwewa1JlCiSCC8dLElmaU2/frLolpgibEY= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.7/go.mod h1:bJnwzfv03oZQeCc863pdGTDgf5nmCy6Za3RAE7d2XsQ= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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/applications/trader-go/main.go b/full-stack-asset-transfer-guide/applications/trader-go/main.go new file mode 100644 index 0000000000..2136c9acb5 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-go/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "errors" + "fmt" + "os" + "sort" +) + +func main() { + if err := run(); err != nil { + var expectedErr *ExpectedError + if errors.As(err, &expectedErr) { + fmt.Println(err) + } else { + fmt.Fprintf(os.Stderr, "\nUnexpected application error: %v\n", err) + os.Exit(1) + } + } +} + +func run() error { + args := os.Args[1:] + if len(args) == 0 { + printUsage() + return fmt.Errorf("no command specified") + } + + commandName := args[0] + commandArgs := args[1:] + + command, ok := commands[commandName] + if !ok { + printUsage() + return fmt.Errorf("unknown command: %s", commandName) + } + + grpcConn, err := newGrpcConnection() + if err != nil { + return fmt.Errorf("failed to create gRPC connection: %w", err) + } + defer grpcConn.Close() + + gw, err := newGatewayConnection(grpcConn) + if err != nil { + return fmt.Errorf("failed to connect to gateway: %w", err) + } + defer gw.Close() + + return command(gw, commandArgs) +} + +func printUsage() { + names := make([]string, 0, len(commands)) + for name := range commands { + names = append(names, name) + } + sort.Strings(names) + + fmt.Println("Arguments: [ ...]") + fmt.Println("Available commands:") + for _, name := range names { + fmt.Printf("\t%s\n", name) + } +}