From be7b275c3fc5f1d66e908c8d73e15c0aea89b88c Mon Sep 17 00:00:00 2001 From: Andrew Coleman Date: Fri, 14 Feb 2020 16:47:05 +0000 Subject: [PATCH] FABG-933 Gateway package for Go SDK (#51) * FABG-933 Gateway package for Go SDK First user story for Gateway SDK Basic framework Unit tests Integration test Signed-off-by: andrew-coleman * FABG-933 Gateway package for Go SDK Incorporate review feedback Signed-off-by: andrew-coleman --- pkg/gateway/contract.go | 63 ++++ pkg/gateway/contract_test.go | 84 ++++++ pkg/gateway/defaultcommithandlers.go | 53 ++++ pkg/gateway/gateway.go | 226 ++++++++++++++ pkg/gateway/gateway_test.go | 277 ++++++++++++++++++ pkg/gateway/inmemorywallet.go | 60 ++++ pkg/gateway/inmemorywallet_test.go | 96 ++++++ pkg/gateway/network.go | 68 +++++ pkg/gateway/network_test.go | 58 ++++ pkg/gateway/spi.go | 39 +++ .../testdata/connection-discovery.json | 62 ++++ pkg/gateway/testdata/connection-tls.json | 133 +++++++++ pkg/gateway/testdata/connection.json | 57 ++++ pkg/gateway/transaction.go | 103 +++++++ pkg/gateway/transaction_test.go | 48 +++ pkg/gateway/wallet.go | 17 ++ pkg/gateway/x509identity.go | 54 ++++ test/fixtures/config/config_e2e.yaml | 6 +- test/integration/pkg/gateway/gateway.go | 131 +++++++++ test/integration/pkg/gateway/gateway_test.go | 23 ++ 20 files changed, 1655 insertions(+), 3 deletions(-) create mode 100644 pkg/gateway/contract.go create mode 100644 pkg/gateway/contract_test.go create mode 100644 pkg/gateway/defaultcommithandlers.go create mode 100644 pkg/gateway/gateway.go create mode 100644 pkg/gateway/gateway_test.go create mode 100644 pkg/gateway/inmemorywallet.go create mode 100644 pkg/gateway/inmemorywallet_test.go create mode 100644 pkg/gateway/network.go create mode 100644 pkg/gateway/network_test.go create mode 100644 pkg/gateway/spi.go create mode 100644 pkg/gateway/testdata/connection-discovery.json create mode 100644 pkg/gateway/testdata/connection-tls.json create mode 100644 pkg/gateway/testdata/connection.json create mode 100644 pkg/gateway/transaction.go create mode 100644 pkg/gateway/transaction_test.go create mode 100644 pkg/gateway/wallet.go create mode 100644 pkg/gateway/x509identity.go create mode 100644 test/integration/pkg/gateway/gateway.go create mode 100644 test/integration/pkg/gateway/gateway_test.go diff --git a/pkg/gateway/contract.go b/pkg/gateway/contract.go new file mode 100644 index 0000000000..92e23f7564 --- /dev/null +++ b/pkg/gateway/contract.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + +// A Contract object represents a smart contract instance in a network. +// Applications should get a Contract instance from a Network using the GetContract method +type Contract struct { + chaincodeID string + name string + network *Network + client *channel.Client +} + +func newContract(network *Network, chaincodeID string, name string) *Contract { + return &Contract{network: network, client: network.client, chaincodeID: chaincodeID, name: name} +} + +// Name returns the name of the smart contract +func (c *Contract) Name() string { + return c.chaincodeID +} + +// EvaluateTransaction will evaluate a transaction function and return its results. +// The transaction function 'name' +// will be evaluated on the endorsing peers but the responses will not be sent to +// the ordering service and hence will not be committed to the ledger. +// This can be used for querying the world state. +func (c *Contract) EvaluateTransaction(name string, args ...string) ([]byte, error) { + txn, err := c.CreateTransaction(name) + + if err != nil { + return nil, err + } + + return txn.Evaluate(args...) +} + +// SubmitTransaction will submit a transaction to the ledger. The transaction function 'name' +// will be evaluated on the endorsing peers and then submitted to the ordering service +// for committing to the ledger. +func (c *Contract) SubmitTransaction(name string, args ...string) ([]byte, error) { + txn, err := c.CreateTransaction(name) + + if err != nil { + return nil, err + } + + return txn.Submit(args...) +} + +// CreateTransaction creates an object representing a specific invocation of a transaction +// function implemented by this contract, and provides more control over +// the transaction invocation using the optional arguments. A new transaction object must +// be created for each transaction invocation. +func (c *Contract) CreateTransaction(name string, args ...TransactionOption) (*Transaction, error) { + return newTransaction(name, c, args...) +} diff --git a/pkg/gateway/contract_test.go b/pkg/gateway/contract_test.go new file mode 100644 index 0000000000..6fb39c0967 --- /dev/null +++ b/pkg/gateway/contract_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "testing" +) + +func TestCreateTransaction(t *testing.T) { + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + contr := nw.GetContract("contract1") + + txn, err := contr.CreateTransaction("txn1") + + if err != nil { + t.Fatalf("Failed to create transaction: %s", err) + } + + name := txn.name + if name != "txn1" { + t.Fatalf("Incorrect transaction name: %s", name) + } +} + +func TestSubmitTransaction(t *testing.T) { + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + contr := nw.GetContract("contract1") + + result, err := contr.SubmitTransaction("txn1") + + if err != nil { + t.Fatalf("Failed to submit transaction: %s", err) + } + + if string(result) != "abc" { + t.Fatalf("Incorrect transaction result: %s", result) + } +} + +func TestEvaluateTransaction(t *testing.T) { + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + contr := nw.GetContract("contract1") + + result, err := contr.EvaluateTransaction("txn1") + + if err != nil { + t.Fatalf("Failed to evaluate transaction: %s", err) + } + + if string(result) != "abc" { + t.Fatalf("Incorrect transaction result: %s", result) + } +} diff --git a/pkg/gateway/defaultcommithandlers.go b/pkg/gateway/defaultcommithandlers.go new file mode 100644 index 0000000000..c60170221e --- /dev/null +++ b/pkg/gateway/defaultcommithandlers.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +type list struct { + None CommitHandlerFactory + OrgAll CommitHandlerFactory + OrgAny CommitHandlerFactory + NetworkAll CommitHandlerFactory + NetworkAny CommitHandlerFactory +} + +// DefaultCommitHandlers provides the built-in commit handler implementations. +var DefaultCommitHandlers = &list{ + None: nil, + OrgAll: orgAll, + OrgAny: orgAny, + NetworkAll: networkAll, + NetworkAny: networkAny, +} + +type commithandler struct { + transactionID string + network Network +} + +func (ch *commithandler) StartListening() { +} + +func (ch *commithandler) WaitForEvents(timeout int64) { +} + +func (ch *commithandler) CancelListening() { +} + +type commithandlerfactory struct { +} + +func (chf *commithandlerfactory) Create(txid string, network Network) CommitHandler { + return &commithandler{ + transactionID: txid, + network: network, + } +} + +var orgAll = &commithandlerfactory{} +var orgAny = &commithandlerfactory{} +var networkAll = &commithandlerfactory{} +var networkAny = &commithandlerfactory{} diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go new file mode 100644 index 0000000000..adc1593052 --- /dev/null +++ b/pkg/gateway/gateway.go @@ -0,0 +1,226 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "fmt" + + "github.com/hyperledger/fabric-sdk-go/pkg/client/msp" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/core" + mspProvider "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/msp" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" + "github.com/pkg/errors" +) + +// Gateway is the entry point to a Fabric network +type Gateway struct { + sdk *fabsdk.FabricSDK + options *gatewayOptions + cfg core.ConfigBackend + org string +} + +type gatewayOptions struct { + Identity mspProvider.SigningIdentity + User string + CommitHandler CommitHandlerFactory + Discovery bool +} + +// Option functional arguments can be supplied when connecting to the gateway. +type Option = func(*Gateway) error + +// ConfigOption specifies the gateway configuration source. +type ConfigOption = func(*Gateway) error + +// IdentityOption specifies the user identity under which all transactions are performed for this gateway instance. +type IdentityOption = func(*Gateway) error + +// Connect to a gateway defined by a network config file. +// Must specify a config option, an identity option and zero or more strategy options. +func Connect(config ConfigOption, identity IdentityOption, options ...Option) (*Gateway, error) { + + g := &Gateway{ + options: &gatewayOptions{ + CommitHandler: DefaultCommitHandlers.OrgAll, + Discovery: true, + }, + } + + err := config(g) + if err != nil { + return nil, errors.Wrap(err, "Failed to apply config option") + } + + err = identity(g) + if err != nil { + return nil, errors.Wrap(err, "Failed to apply identity option") + } + + for _, option := range options { + err = option(g) + if err != nil { + return nil, errors.Wrap(err, "Failed to apply gateway option") + } + } + + return g, nil +} + +// WithConfig configures the gateway from a network config, such as a ccp file. +func WithConfig(config core.ConfigProvider) ConfigOption { + return func(gw *Gateway) error { + var err error + sdk, err := fabsdk.New(config) + + if err != nil { + return err + } + + gw.sdk = sdk + + configBackend, err := config() + if err != nil { + return err + } + if len(configBackend) != 1 { + return errors.New("invalid config file") + } + + cfg := configBackend[0] + gw.cfg = cfg + + value, ok := cfg.Lookup("client.organization") + if !ok { + return errors.New("No client organization defined in the config") + } + gw.org = value.(string) + + return nil + } +} + +// WithSDK configures the gateway with the configuration from an existing FabricSDK instance +func WithSDK(sdk *fabsdk.FabricSDK) ConfigOption { + return func(gw *Gateway) error { + gw.sdk = sdk + + cfg, err := sdk.Config() + + if err != nil { + return errors.Wrap(err, "Unable to access SDK configuration") + } + + value, ok := cfg.Lookup("client.organization") + if !ok { + return errors.New("No client organization defined in the config") + } + gw.org = value.(string) + + return nil + } +} + +// WithIdentity is an optional argument to the Connect method which specifies +// the identity that is to be used to connect to the network. +// All operations under this gateway connection will be performed using this identity. +func WithIdentity(wallet Wallet, label string) IdentityOption { + return func(gw *Gateway) error { + mspClient, err := msp.New(gw.getSDK().Context(), msp.WithOrg(gw.getOrg())) + if err != nil { + return err + } + + creds, err := wallet.Get(label) + if err != nil { + return err + } + + var identity mspProvider.SigningIdentity + switch v := creds.(type) { + case *X509Identity: + identity, err = mspClient.CreateSigningIdentity(mspProvider.WithCert([]byte(v.Cert())), mspProvider.WithPrivateKey([]byte(v.Key()))) + if err != nil { + return err + } + } + + gw.options.Identity = identity + return nil + } +} + +// WithUser is an optional argument to the Connect method which specifies +// the identity that is to be used to connect to the network. +// All operations under this gateway connection will be performed using this identity. +func WithUser(user string) IdentityOption { + return func(gw *Gateway) error { + gw.options.User = user + return nil + } +} + +// WithCommitHandler is an optional argument to the Connect method which +// allows an alternative commit handler to be specified. The commit handler defines how +// client code should wait to receive commit events from peers following submit of a transaction. +// Currently unimplemented. +func WithCommitHandler(handler CommitHandlerFactory) Option { + return func(gw *Gateway) error { + gw.options.CommitHandler = handler + return nil + } +} + +// WithDiscovery is an optional argument to the Connect method which +// enables or disables service discovery for all transaction submissions for this gateway. +func WithDiscovery(discovery bool) Option { + return func(gw *Gateway) error { + gw.options.Discovery = discovery + return nil + } +} + +func (gw *Gateway) getSDK() *fabsdk.FabricSDK { + return gw.sdk +} + +func (gw *Gateway) getOrg() string { + return gw.org +} + +func (gw *Gateway) getPeersForOrg(org string) ([]string, error) { + value, ok := gw.cfg.Lookup("organizations." + org + ".peers") + if !ok { + return nil, errors.New("No client organization defined in the config") + } + + val := value.([]interface{}) + s := make([]string, len(val)) + for i, v := range val { + s[i] = fmt.Sprint(v) + } + + return s, nil +} + +// GetNetwork returns an object representing a network channel. +func (gw *Gateway) GetNetwork(name string) (*Network, error) { + var channelProvider context.ChannelProvider + if gw.options.Identity != nil { + channelProvider = gw.sdk.ChannelContext(name, fabsdk.WithIdentity(gw.options.Identity), fabsdk.WithOrg(gw.org)) + } else { + channelProvider = gw.sdk.ChannelContext(name, fabsdk.WithUser(gw.options.User), fabsdk.WithOrg(gw.org)) + } + return newNetwork(gw, channelProvider) +} + +// Close the gateway connection and all associated resources, including removing listeners attached to networks and +// contracts created by the gateway. +func (gw *Gateway) Close() { + // future use +} diff --git a/pkg/gateway/gateway_test.go b/pkg/gateway/gateway_test.go new file mode 100644 index 0000000000..548e07caed --- /dev/null +++ b/pkg/gateway/gateway_test.go @@ -0,0 +1,277 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "reflect" + "testing" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" +) + +const testPrivKey string = `-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQggkuKP0YNrbuilpFf +0F/I+3At9LZh6EysU8lVBuy+cregCgYIKoZIzj0DAQehRANCAAQ3NMOS6YpCyFKJ +jgKYCP6eQYUG91jdhoQK+8Ufhy0/V/CVdJj/Exe89yzAqKfLzb9tc6MuWOYLwPRD +sF3d8qsw +-----END PRIVATE KEY-----` + +const testCert string = `-----BEGIN CERTIFICATE----- +MIICjzCCAjWgAwIBAgIUXtE0iOex19qEbY12PpU3Sig3/LswCgYIKoZIzj0EAwIw +czELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT +E2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMjAwMTA3MTEzNjAwWhcNMjEwMTA2MTE0 +MTAwWjBCMTAwDQYDVQQLEwZjbGllbnQwCwYDVQQLEwRvcmcxMBIGA1UECxMLZGVw +YXJ0bWVudDExDjAMBgNVBAMTBXVzZXIxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAENzTDkumKQshSiY4CmAj+nkGFBvdY3YaECvvFH4ctP1fwlXSY/xMXvPcswKin +y82/bXOjLljmC8D0Q7Bd3fKrMKOB1zCB1DAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0T +AQH/BAIwADAdBgNVHQ4EFgQUfi/LNRJof+w9YtBydB7kpget9eowKwYDVR0jBCQw +IoAga001uwQc4mqKCzZzSlqHrmd3JGYF3lbyxsEzYHvzmSEwaAYIKgMEBQYHCAEE +XHsiYXR0cnMiOnsiaGYuQWZmaWxpYXRpb24iOiJvcmcxLmRlcGFydG1lbnQxIiwi +aGYuRW5yb2xsbWVudElEIjoidXNlcjEiLCJoZi5UeXBlIjoiY2xpZW50In19MAoG +CCqGSM49BAMCA0gAMEUCIQCXMS8+ahDQZ5wHnWUcps9GH2uWG+qPO3LxTitCH/rs +owIgRo0pFBhgLXaJ9ECYR+gSNBDpIc5I/Fr7QL7iIleSQlY= +-----END CERTIFICATE-----` + +func TestConnectIdentityInCcp(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + if gw == nil { + t.Fatal("Failed to create gateway") + } +} + +func TestConnectNoOptions(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + options := gw.options + + if options.CommitHandler != DefaultCommitHandlers.OrgAll { + t.Fatal("DefaultCommitHandler not correctly initialized") + } + + if options.Discovery != true { + t.Fatal("Discovery not correctly initialized") + } +} + +func TestConnectWithSDK(t *testing.T) { + sdk, err := fabsdk.New(config.FromFile("testdata/connection-tls.json")) + + if err != nil { + t.Fatalf("Failed to create SDK: %s", err) + } + + gw, err := Connect( + WithSDK(sdk), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + options := gw.options + + if options.CommitHandler != DefaultCommitHandlers.OrgAll { + t.Fatal("DefaultCommitHandler not correctly initialized") + } + + if options.Discovery != true { + t.Fatal("Discovery not correctly initialized") + } +} + +func TestConnectWithIdentity(t *testing.T) { + wallet := NewInMemoryWallet() + wallet.Put("user", NewX509Identity(testCert, testPrivKey)) + + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithIdentity(wallet, "user"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + if gw.options.Identity == nil { + t.Fatal("Identity not set") + } + + mspid := gw.options.Identity.Identifier().MSPID + + if !reflect.DeepEqual(mspid, "Org1MSP") { + t.Fatalf("Incorrect mspid: %s", mspid) + } +} + +func TestConnectWithCommitHandler(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + WithCommitHandler(DefaultCommitHandlers.OrgAny), + ) + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + options := gw.options + + if options.CommitHandler != DefaultCommitHandlers.OrgAny { + t.Fatal("CommitHandler not set correctly") + } +} + +func TestConnectWithDiscovery(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + WithDiscovery(false), + ) + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + options := gw.options + + if options.Discovery != false { + t.Fatal("Discovery not set correctly") + } +} + +func TestConnectWithMultipleOptions(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + WithCommitHandler(DefaultCommitHandlers.OrgAny), + WithDiscovery(false), + ) + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + options := gw.options + + if options.Discovery != false { + t.Fatal("Discovery not set correctly") + } + + if options.CommitHandler != DefaultCommitHandlers.OrgAny { + t.Fatal("CommitHandler not set correctly") + } +} + +func TestGetSDK(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + if gw.getSDK() != gw.sdk { + t.Fatal("getSDK() not returning the correct object") + } +} + +func TestGetOrg(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + org := gw.getOrg() + + if org != "Org1" { + t.Fatalf("getOrg() returns: %s", org) + } +} + +func TestGetPeersForOrg(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + peers, err := gw.getPeersForOrg("Org1") + + if err != nil { + t.Fatalf("Failed to get peers for org: %s", err) + } + + if reflect.DeepEqual(peers, [1]string{"peer0.org1.example.com"}) { + t.Fatalf("GetPeersForOrg(Org1) returns: %s", peers) + } + + peers, err = gw.getPeersForOrg("Org2") + + if reflect.DeepEqual(peers, [1]string{"peer0.org2.example.com"}) { + t.Fatalf("GetPeersForOrg(Org1) returns: %s", peers) + } + + peers, err = gw.getPeersForOrg("Org3") + + if err == nil { + t.Fatal("GetPeersForOrg(Org3) should have returned error") + } +} + +func TestGetNetwork(t *testing.T) { + wallet := NewInMemoryWallet() + wallet.Put("user", NewX509Identity(testCert, testPrivKey)) + + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithIdentity(wallet, "user")) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + _, err = gw.GetNetwork("mychannel") + if err == nil { + t.Fatalf("Failed to get network: %s", err) + } +} + +func TestClose(t *testing.T) { + gw, err := Connect( + WithConfig(config.FromFile("testdata/connection-tls.json")), + WithUser("user1"), + ) + + if err != nil { + t.Fatalf("Failed to create gateway: %s", err) + } + + gw.Close() +} diff --git a/pkg/gateway/inmemorywallet.go b/pkg/gateway/inmemorywallet.go new file mode 100644 index 0000000000..d28af74318 --- /dev/null +++ b/pkg/gateway/inmemorywallet.go @@ -0,0 +1,60 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import "errors" + +// InMemoryWallet stores identity information used to connect to a Hyperledger Fabric network. +// Instances are created using NewInMemoryWallet() +type InMemoryWallet struct { + idhandler IDHandler + storage map[string]map[string]string +} + +// NewInMemoryWallet creates an instance of a wallet, backed by files on the filesystem +func NewInMemoryWallet() *InMemoryWallet { + return &InMemoryWallet{newX509IdentityHandler(), make(map[string]map[string]string, 10)} +} + +// Put an identity into the wallet. +func (f *InMemoryWallet) Put(label string, id IdentityType) error { + elements := f.idhandler.GetElements(id) + f.storage[label] = elements + return nil +} + +// Get an identity from the wallet. +func (f *InMemoryWallet) Get(label string) (IdentityType, error) { + if elements, ok := f.storage[label]; ok { + return f.idhandler.FromElements(elements), nil + } + return nil, errors.New("label doesn't exist: " + label) +} + +// Remove an identity from the wallet. If the identity does not exist, this method does nothing. +func (f *InMemoryWallet) Remove(label string) error { + if _, ok := f.storage[label]; ok { + delete(f.storage, label) + return nil + } + return nil // what should we do here ? +} + +// Exists returns true if the identity is in the wallet. +func (f *InMemoryWallet) Exists(label string) bool { + _, ok := f.storage[label] + return ok +} + +// List all of the labels in the wallet. +func (f *InMemoryWallet) List() []string { + labels := make([]string, 0, len(f.storage)) + for label := range f.storage { + labels = append(labels, label) + } + return labels +} diff --git a/pkg/gateway/inmemorywallet_test.go b/pkg/gateway/inmemorywallet_test.go new file mode 100644 index 0000000000..bb491898ea --- /dev/null +++ b/pkg/gateway/inmemorywallet_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "reflect" + "sort" + "testing" +) + +func TestNewInMemoryWallet(t *testing.T) { + wallet := NewInMemoryWallet() + if wallet == nil { + t.Fatal("Failed to create in memory wallet") + } +} + +func TestInsertionAndExistance(t *testing.T) { + wallet := NewInMemoryWallet() + wallet.Put("label1", NewX509Identity("testCert", "testPrivKey")) + exists := wallet.Exists("label1") + if exists != true { + t.Fatal("Expected label1 to be in wallet") + } +} + +func TestNonExistance(t *testing.T) { + wallet := NewInMemoryWallet() + exists := wallet.Exists("label1") + if exists != false { + t.Fatal("Expected label1 to not be in wallet") + } +} + +func TestLookupNonExist(t *testing.T) { + wallet := NewInMemoryWallet() + _, err := wallet.Get("label1") + if err == nil { + t.Fatal("Expected error for label1 not in wallet") + } +} + +func TestInsertionAndLookup(t *testing.T) { + wallet := NewInMemoryWallet() + wallet.Put("label1", NewX509Identity("testCert", "testPrivKey")) + entry, err := wallet.Get("label1") + if err != nil { + t.Fatalf("Failed to lookup identity: %s", err) + } + if entry.Type() != "X509" { + t.Fatalf("Unexpected identity type: %s", entry.Type()) + } +} + +func TestContentsOfWallet(t *testing.T) { + wallet := NewInMemoryWallet() + contents := wallet.List() + if len(contents) != 0 { + t.Fatal("Wallet should be empty") + } + wallet.Put("label1", NewX509Identity("testCert", "testPrivKey")) + wallet.Put("label2", NewX509Identity("testCert", "testPrivKey")) + contents = wallet.List() + sort.Strings(contents) + expected := []string{"label1", "label2"} + if !reflect.DeepEqual(contents, expected) { + t.Fatalf("Unexpected wallet contents: %s", contents) + } +} + +func TestRemovalFromWallet(t *testing.T) { + wallet := NewInMemoryWallet() + contents := wallet.List() + wallet.Put("label1", NewX509Identity("testCert1", "testPrivKey")) + wallet.Put("label2", NewX509Identity("testCert2", "testPrivKey")) + wallet.Put("label3", NewX509Identity("testCert3", "testPrivKey")) + wallet.Remove("label2") + contents = wallet.List() + sort.Strings(contents) + expected := []string{"label1", "label3"} + if !reflect.DeepEqual(contents, expected) { + t.Fatalf("Unexpected wallet contents: %s", contents) + } +} + +func TestRemoveNonExist(t *testing.T) { + wallet := NewInMemoryWallet() + err := wallet.Remove("label1") + if err != nil { + t.Fatal("Remove should not throw error for non-existant label") + } +} diff --git a/pkg/gateway/network.go b/pkg/gateway/network.go new file mode 100644 index 0000000000..85a69fabbc --- /dev/null +++ b/pkg/gateway/network.go @@ -0,0 +1,68 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context" + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab" + "github.com/pkg/errors" +) + +// A Network object represents the set of peers in a Fabric network (channel). +// Applications should get a Network instance from a Gateway using the GetNetwork method. +type Network struct { + name string + gateway *Gateway + client *channel.Client + peers []fab.Peer +} + +func newNetwork(gateway *Gateway, channelProvider context.ChannelProvider) (*Network, error) { + n := Network{ + gateway: gateway, + } + + // Channel client is used to query and execute transactions + client, err := channel.New(channelProvider) + if err != nil { + return nil, errors.Wrap(err, "Failed to create new channel client") + } + + n.client = client + + ctx, err := channelProvider() + if err != nil { + return nil, errors.Wrap(err, "Failed to create new channel context") + } + + n.name = ctx.ChannelID() + + discovery, err := ctx.ChannelService().Discovery() + if err != nil { + return nil, errors.Wrap(err, "Failed to create discovery service") + } + + peers, err := discovery.GetPeers() + if err != nil { + return nil, errors.Wrap(err, "Failed to discover peers") + } + + n.peers = peers + + return &n, nil +} + +// Name is the name of the network (also known as channel name) +func (n *Network) Name() string { + return n.name +} + +// GetContract returns instance of a smart contract on the current network. +func (n *Network) GetContract(chaincodeID string) *Contract { + return newContract(n, chaincodeID, "") +} diff --git a/pkg/gateway/network_test.go b/pkg/gateway/network_test.go new file mode 100644 index 0000000000..fe2f745d0b --- /dev/null +++ b/pkg/gateway/network_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "testing" + + "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context" + "github.com/hyperledger/fabric-sdk-go/pkg/fab/mocks" +) + +func TestNewNetwork(t *testing.T) { + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + if nw.Name() != "mychannel" { + t.Fatalf("Incorrect network name: %s", nw.Name()) + } +} + +func TestGetContract(t *testing.T) { + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + contr := nw.GetContract("contract1") + name := contr.Name() + + if name != "contract1" { + t.Fatalf("Incorrect contract name: %s", err) + } +} + +func mockChannelProvider(channelID string) context.ChannelProvider { + + channelProvider := func() (context.Channel, error) { + return mocks.NewMockChannel(channelID) + } + + return channelProvider +} diff --git a/pkg/gateway/spi.go b/pkg/gateway/spi.go new file mode 100644 index 0000000000..9978759f1b --- /dev/null +++ b/pkg/gateway/spi.go @@ -0,0 +1,39 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +// This contains the service provider interface (SPI) which provides the mechanism +// for implementing alternative gateway strategies, wallets, etc. +// This is currently experimental and will be implemented in future user stories + +// CommitHandlerFactory is currently unimplemented +type CommitHandlerFactory interface { + Create(string, Network) CommitHandler +} + +// CommitHandler is currently unimplemented +type CommitHandler interface { + StartListening() + WaitForEvents(int64) + CancelListening() +} + +// Identity is the base type for implementing wallet identities - experimental +type Identity struct { + theType string +} + +// IdentityType represents a specific identity format - experimental +type IdentityType interface { + Type() string +} + +// IDHandler represents the storage of identity information - experimental +type IDHandler interface { + GetElements(id IdentityType) map[string]string + FromElements(map[string]string) IdentityType +} diff --git a/pkg/gateway/testdata/connection-discovery.json b/pkg/gateway/testdata/connection-discovery.json new file mode 100644 index 0000000000..aabe405b31 --- /dev/null +++ b/pkg/gateway/testdata/connection-discovery.json @@ -0,0 +1,62 @@ +{ + "name": "basic-network", + "version": "1.0.0", + "client": { + "organization": "Org1", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + }, + "orderer": "300" + } + } + }, + "organizations": { + "Org1": { + "mspid": "Org1MSP", + "peers": [ + "peer0.org1.example.com" + ], + "certificateAuthorities": [ + "ca-org1" + ], + "adminPrivateKeyPEM": { + "path": "src/test/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/key.pem" + }, + "signedCertPEM": { + "path": "src/test/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem" + } + } + }, + "peers": { + "peer0.org1.example.com": { + "url": "grpcs://peer0.org1.example.com:7051", + "grpcOptions": { + "hostnameOverride": "peer0.org1.example.com", + "ssl-target-name-override": "peer0.org1.example.com", + "request-timeout": 120001 + }, + "tlsCACerts": { + "path": "src/test/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" + } + } + }, + "certificateAuthorities": { + "ca-org1": { + "url": "https://localhost:7054", + "grpcOptions": { + "verify": true + }, + "tlsCACerts": { + "path": "src/test/fixtures/crypto-material/crypto-config/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem" + }, + "registrar": [ + { + "enrollId": "admin", + "enrollSecret": "adminpw" + } + ] + } + } +} diff --git a/pkg/gateway/testdata/connection-tls.json b/pkg/gateway/testdata/connection-tls.json new file mode 100644 index 0000000000..aa0225154f --- /dev/null +++ b/pkg/gateway/testdata/connection-tls.json @@ -0,0 +1,133 @@ +{ + "name": "basic-network", + "version": "1.0.0", + "client": { + "organization": "Org1", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + }, + "orderer": "300" + } + }, + "cryptoconfig": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}" + } + }, + "channels": { + "mychannel": { + "peers": { + "badpeer.org1.example.com": { + "endorsingPeer": true, + "chaincodeQuery": true, + "ledgerQuery": true, + "eventSource": true + }, + "peer0.org1.example.com": { + "endorsingPeer": true, + "chaincodeQuery": true, + "ledgerQuery": true, + "eventSource": true + }, + "peer0.org2.example.com": { + "endorsingPeer": true, + "chaincodeQuery": false, + "ledgerQuery": true, + "eventSource": false + } + } + } + }, + "organizations": { + "Org1": { + "mspid": "Org1MSP", + "cryptoPath": "peerOrganizations/org1.example.com/users/{username}@org1.example.com/msp", + "peers": [ + "peer0.org1.example.com" + ], + "certificateAuthorities": [ + "ca-org1" + ] + }, + "Org2": { + "mspid": "Org2MSP", + "cryptoPath": "peerOrganizations/org2.example.com/users/{username}@org2.example.com/msp", + "peers": [ + "peer0.org2.example.com" + ], + "certificateAuthorities": [ + "ca-org2" + ] + } + }, + "orderers": { + "orderer.example.com": { + "url": "grpcs://localhost:7050", + "mspid": "OrdererMSP", + "grpcOptions": { + "ssl-target-name-override": "orderer.example.com", + "hostnameOverride": "orderer.example.com" + }, + "tlsCACerts": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt" + } + } + }, + "peers": { + "peer0.org1.example.com": { + "url": "grpcs://localhost:7051", + "grpcOptions": { + "ssl-target-name-override": "peer0.org1.example.com", + "hostnameOverride": "peer0.org1.example.com", + "request-timeout": 120001 + }, + "tlsCACerts": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" + } + }, + "peer0.org2.example.com": { + "url": "grpcs://localhost:9051", + "grpcOptions": { + "ssl-target-name-override": "peer0.org2.example.com", + "hostnameOverride": "peer0.org2.example.com", + "request-timeout": 120001 + }, + "tlsCACerts": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" + } + } + }, + "certificateAuthorities": { + "ca-org1": { + "url": "https://localhost:7054", + "grpcOptions": { + "verify": true + }, + "tlsCACerts": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem" + }, + "registrar": + { + "enrollId": "admin", + "enrollSecret": "adminpw" + } + + }, + "ca-org2": { + "url": "https://localhost:8054", + "grpcOptions": { + "verify": true + }, + "tlsCACerts": { + "path": "${FABRIC_SDK_GO_PROJECT_PATH}/${CRYPTOCONFIG_FIXTURES_PATH}/peerOrganizations/org2.example.com/ca/ca.org2.example.com-cert.pem" + }, + "registrar": + { + "enrollId": "admin", + "enrollSecret": "adminpw" + } + + } + } +} diff --git a/pkg/gateway/testdata/connection.json b/pkg/gateway/testdata/connection.json new file mode 100644 index 0000000000..8791b634a1 --- /dev/null +++ b/pkg/gateway/testdata/connection.json @@ -0,0 +1,57 @@ +{ + "name": "basic-network", + "version": "1.0.0", + "client": { + "organization": "Org1", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + }, + "orderer": "300" + } + } + }, + "channels": { + "mychannel": { + "orderers": [ + "orderer.example.com" + ], + "peers": { + "peer0.org1.example.com": { + "endorsingPeer": true, + "chaincodeQuery": true, + "ledgerQuery": true, + "eventSource": true + } + } + } + }, + "organizations": { + "Org1": { + "mspid": "Org1MSP", + "peers": [ + "peer0.org1.example.com" + ], + "certificateAuthorities": [ + "ca.example.com" + ] + } + }, + "orderers": { + "orderer.example.com": { + "url": "grpc://localhost:7050" + } + }, + "peers": { + "peer0.org1.example.com": { + "url": "grpc://localhost:7051" + } + }, + "certificateAuthorities": { + "ca.example.com": { + "url": "http://localhost:7054", + "caName": "ca.example.com" + } + } +} diff --git a/pkg/gateway/transaction.go b/pkg/gateway/transaction.go new file mode 100644 index 0000000000..e2c71723be --- /dev/null +++ b/pkg/gateway/transaction.go @@ -0,0 +1,103 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "github.com/hyperledger/fabric-sdk-go/pkg/client/channel" + "github.com/pkg/errors" +) + +// A Transaction represents a specific invocation of a transaction function, and provides +// flexibility over how that transaction is invoked. Applications should +// obtain instances of this class from a Contract using the +// Contract.CreateTransaction method. +// +// Instances of this class are stateful. A new instance must +// be created for each transaction invocation. +type Transaction struct { + name string + contract *Contract + request *channel.Request + endorsingPeers []string +} + +// TransactionOption functional arguments can be supplied when creating a transaction object +type TransactionOption = func(*Transaction) error + +func newTransaction(name string, contract *Contract, options ...TransactionOption) (*Transaction, error) { + txn := &Transaction{ + name: name, + contract: contract, + request: &channel.Request{ChaincodeID: contract.chaincodeID, Fcn: name}, + } + + for _, option := range options { + err := option(txn) + if err != nil { + return nil, err + } + } + + return txn, nil +} + +// WithTransient is an optional argument to the CreateTransaction method which +// sets the transient data that will be passed to the transaction function +// but will not be stored on the ledger. This can be used to pass +// private data to a transaction function. +func WithTransient(data map[string][]byte) TransactionOption { + return func(txn *Transaction) error { + txn.request.TransientMap = data + return nil + } +} + +// WithEndorsingPeers is an optional argument to the CreateTransaction method which +// sets the peers that should be used for endorsement of transaction submitted to the ledger using Submit() +func WithEndorsingPeers(peers ...string) TransactionOption { + return func(txn *Transaction) error { + txn.endorsingPeers = peers + return nil + } +} + +// Evaluate a transaction function and return its results. +// The transaction function will be evaluated on the endorsing peers but +// the responses will not be sent to the ordering service and hence will +// not be committed to the ledger. This can be used for querying the world state. +func (txn *Transaction) Evaluate(args ...string) ([]byte, error) { + bytes := make([][]byte, len(args)) + for i, v := range args { + bytes[i] = []byte(v) + } + txn.request.Args = bytes + + response, err := txn.contract.client.Query(*txn.request, channel.WithTargets(txn.contract.network.peers[0])) + if err != nil { + return nil, errors.Wrap(err, "Failed to evaluate") + } + + return response.Payload, nil +} + +// Submit a transaction to the ledger. The transaction function represented by this object +// will be evaluated on the endorsing peers and then submitted to the ordering service +// for committing to the ledger. +func (txn *Transaction) Submit(args ...string) ([]byte, error) { + bytes := make([][]byte, len(args)) + for i, v := range args { + bytes[i] = []byte(v) + } + txn.request.Args = bytes + + response, err := txn.contract.client.Execute(*txn.request) + if err != nil { + return nil, errors.Wrap(err, "Failed to submit") + } + + return response.Payload, nil +} diff --git a/pkg/gateway/transaction_test.go b/pkg/gateway/transaction_test.go new file mode 100644 index 0000000000..ebb2368b05 --- /dev/null +++ b/pkg/gateway/transaction_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "testing" +) + +func TestTransactionOptions(t *testing.T) { + transient := make(map[string][]byte) + transient["price"] = []byte("8500") + + c := mockChannelProvider("mychannel") + + gw := &Gateway{} + + nw, err := newNetwork(gw, c) + + if err != nil { + t.Fatalf("Failed to create network: %s", err) + } + + contr := nw.GetContract("contract1") + + txn, err := contr.CreateTransaction( + "txn1", + WithTransient(transient), + WithEndorsingPeers("peer1"), + ) + + if err != nil { + t.Fatalf("Failed to create transaction: %s", err) + } + + data := txn.request.TransientMap["price"] + if string(data) != "8500" { + t.Fatalf("Incorrect transient data: %s", string(data)) + } + + endorsers := txn.endorsingPeers + if endorsers[0] != "peer1" { + t.Fatalf("Incorrect endorsing peer: %s", endorsers[0]) + } +} diff --git a/pkg/gateway/wallet.go b/pkg/gateway/wallet.go new file mode 100644 index 0000000000..57a85e8989 --- /dev/null +++ b/pkg/gateway/wallet.go @@ -0,0 +1,17 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +// A Wallet stores identity information used to connect to a Hyperledger Fabric network. +// Instances are created using factory methods on the implementing objects. +type Wallet interface { + Put(label string, id IdentityType) error + Get(label string) (IdentityType, error) + Remove(label string) error + Exists(label string) bool + List() []string +} diff --git a/pkg/gateway/x509identity.go b/pkg/gateway/x509identity.go new file mode 100644 index 0000000000..9d7b810061 --- /dev/null +++ b/pkg/gateway/x509identity.go @@ -0,0 +1,54 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +// X509Identity represents an X509 identity +type X509Identity struct { + Identity + cert string + key string +} + +// Type returns X509 for this identity type +func (x *X509Identity) Type() string { + return "X509" +} + +// Cert returns the X509 certificate PEM +func (x *X509Identity) Cert() string { + return x.cert +} + +// Key returns the private key PEM +func (x *X509Identity) Key() string { + return x.key +} + +// NewX509Identity creates an X509 identity for storage in a wallet +func NewX509Identity(cert string, key string) *X509Identity { + return &X509Identity{Identity{"X509"}, cert, key} +} + +type x509IdentityHandler struct{} + +func (x *x509IdentityHandler) GetElements(id IdentityType) map[string]string { + r, _ := id.(*X509Identity) + + return map[string]string{ + "cert": r.cert, + "key": r.key, + } +} + +func (x *x509IdentityHandler) FromElements(elements map[string]string) IdentityType { + y := &X509Identity{Identity{"X509"}, elements["cert"], elements["key"]} + return y +} + +func newX509IdentityHandler() *x509IdentityHandler { + return &x509IdentityHandler{} +} diff --git a/test/fixtures/config/config_e2e.yaml b/test/fixtures/config/config_e2e.yaml index b1d4ec7f07..715138230e 100644 --- a/test/fixtures/config/config_e2e.yaml +++ b/test/fixtures/config/config_e2e.yaml @@ -22,7 +22,7 @@ client: # Which organization does this application instance belong to? The value must be the name of an org # defined under "organizations" - organization: org1 + organization: Org1 logging: level: info @@ -254,7 +254,7 @@ channels: # list of participating organizations in this network # organizations: - org1: + Org1: mspid: Org1MSP # This org's MSP store (absolute path or relative to client.cryptoconfig) @@ -277,7 +277,7 @@ organizations: # peers with a public URL to send transaction proposals. The file will not contain private # information reserved for members of the organization, such as admin key and certificate, # fabric-ca registrar enroll ID and secret, etc. - org2: + Org2: mspid: Org2MSP # This org's MSP store (absolute path or relative to client.cryptoconfig) diff --git a/test/integration/pkg/gateway/gateway.go b/test/integration/pkg/gateway/gateway.go new file mode 100644 index 0000000000..a55e968a73 --- /dev/null +++ b/test/integration/pkg/gateway/gateway.go @@ -0,0 +1,131 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "strconv" + "testing" + "time" + + "github.com/hyperledger/fabric-sdk-go/pkg/core/config" + "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk" + "github.com/hyperledger/fabric-sdk-go/pkg/gateway" + "github.com/hyperledger/fabric-sdk-go/test/integration" + "github.com/hyperledger/fabric-sdk-go/test/metadata" +) + +const ( + channelID = "mychannel" +) + +var ( + ccID = "example_cc_e2e" + metadata.TestRunID +) + +// RunWithConfig the basic gateway integration test +func RunWithConfig(t *testing.T) { + configPath := integration.GetConfigPath("config_e2e.yaml") + + gw, err := gateway.Connect( + gateway.WithConfig(config.FromFile(configPath)), + gateway.WithUser("User1"), + ) + + if err != nil { + t.Fatalf("Failed to create new Gateway: %s", err) + } + defer gw.Close() + + nw, err := gw.GetNetwork(channelID) + if err != nil { + t.Fatalf("Failed to get network: %s", err) + } + + name := nw.Name() + if name != channelID { + t.Fatalf("Incorrect network name: %s", name) + } + + contract := nw.GetContract(ccID) + + name = contract.Name() + if name != ccID { + t.Fatalf("Incorrect contract name: %s", name) + } + + runContract(contract, t) +} + +// RunWithSDK the sdk compatibility gateway integration test +func RunWithSDK(t *testing.T) { + configPath := integration.GetConfigPath("config_e2e.yaml") + + sdk, err := fabsdk.New(config.FromFile(configPath)) + + if err != nil { + t.Fatalf("Failed to create new SDK: %s", err) + } + + gw, err := gateway.Connect( + gateway.WithSDK(sdk), + gateway.WithUser("User1"), + ) + + if err != nil { + t.Fatalf("Failed to create new Gateway: %s", err) + } + defer gw.Close() + + nw, err := gw.GetNetwork(channelID) + if err != nil { + t.Fatalf("Failed to get network: %s", err) + } + + name := nw.Name() + if name != channelID { + t.Fatalf("Incorrect network name: %s", name) + } + + contract := nw.GetContract(ccID) + + name = contract.Name() + if name != ccID { + t.Fatalf("Incorrect contract name: %s", name) + } + + runContract(contract, t) +} + +func runContract(contract *gateway.Contract, t *testing.T) { + response, err := contract.EvaluateTransaction("invoke", "query", "b") + + if err != nil { + t.Fatalf("Failed to query funds: %s", err) + } + + value, _ := strconv.Atoi(string(response)) + + _, err = contract.SubmitTransaction("invoke", "move", "a", "b", "1") + + if err != nil { + t.Fatalf("Failed to move funds: %s", err) + } + + time.Sleep(10 * time.Second) + + response, err = contract.EvaluateTransaction("invoke", "query", "b") + + if err != nil { + t.Fatalf("Failed to query funds: %s", err) + } + + newvalue, _ := strconv.Atoi(string(response)) + + if newvalue != value+1 { + t.Fatalf("Incorrect response: %s", response) + } +} diff --git a/test/integration/pkg/gateway/gateway_test.go b/test/integration/pkg/gateway/gateway_test.go new file mode 100644 index 0000000000..0c53bb2719 --- /dev/null +++ b/test/integration/pkg/gateway/gateway_test.go @@ -0,0 +1,23 @@ +/* +Copyright 2020 IBM All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package gateway + +import ( + "testing" +) + +func TestGatewayFromConfig(t *testing.T) { + t.Run("Base", func(t *testing.T) { + RunWithConfig(t) + }) +} + +func TestGatewayFromSDK(t *testing.T) { + t.Run("Base", func(t *testing.T) { + RunWithSDK(t) + }) +}