Repository: hyperledger/firefly
Pull request #887 (2022-07-06 19:46:08)

Add E2E tests for gateway mode


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 522824e9c..6472a53cc 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -29,23 +29,46 @@ jobs:
   e2e-test:
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
-        test-suite: [TestEthereumE2ESuite, TestFabricE2ESuite]
         blockchain-provider: [geth, fabric]
-        token-provider: [none, erc1155, erc20_erc721]
-        database-type: [sqlite3, postgres]
-        exclude:
-          - blockchain-provider: geth
-            test-suite: TestFabricE2ESuite
-          - blockchain-provider: fabric
-            test-suite: TestEthereumE2ESuite
-          - blockchain-provider: fabric
-            token-provider: erc1155
-          - blockchain-provider: fabric
-            token-provider: erc20_erc721
-          - blockchain-provider: geth
-            token-provider: none
-      fail-fast: false
+        exclude: [ blockchain-provider: geth, blockchain-provider: fabric ]
+        include:
+            - blockchain-provider: geth
+              test-suite: TestEthereumMultipartyE2ESuite
+              database-type: sqlite3
+              token-provider: erc20_erc721
+              multiparty-enabled: true
+
+            - blockchain-provider: geth
+              test-suite: TestEthereumMultipartyE2ESuite
+              database-type: postgres
+              token-provider: erc20_erc721
+              multiparty-enabled: true
+            
+            - blockchain-provider: geth
+              test-suite: TestEthereumMultipartyE2ESuite
+              database-type: sqlite3
+              token-provider: erc1155
+              multiparty-enabled: true
+
+            - blockchain-provider: fabric
+              test-suite: TestFabricE2ESuite
+              database-type: sqlite3
+              token-provider: none
+              multiparty-enabled: true
+
+            - blockchain-provider: geth
+              test-suite: TestEthereumGatewayE2ESuite
+              database-type: sqlite3
+              token-provider: erc20_erc721
+              multiparty-enabled: false
+
+            - blockchain-provider: fabric
+              test-suite: TestFabricGatewayE2ESuite
+              database-type: sqlite3
+              token-provider: none
+              multiparty-enabled: false
     steps:
       - uses: actions/checkout@v2
         with:
@@ -62,6 +85,7 @@ jobs:
           BLOCKCHAIN_PROVIDER: ${{ matrix.blockchain-provider }}
           TOKENS_PROVIDER: ${{ matrix.token-provider }}
           DATABASE_TYPE: ${{ matrix.database-type }}
+          MULTIPARTY_ENABLED: ${{ matrix.multiparty-enabled }}
         run: ./test/e2e/run.sh
 
       - name: Archive container logs
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0888a22f5..dc280359e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -8,11 +8,11 @@
       "name": "Debug E2E Tests",
       "type": "go",
       "request": "launch",
-      "mode": "debug",
-      "program": "${workspaceFolder}/test/e2e",
-      "args": ["-test.run"],
+      "mode": "test",
+      "program": "${fileDirname}",
       "env": {
-        "STACK_FILE": "{env:HOME}/.firefly/stacks/firefly_e2e/stack.json"
+        "STACK_FILE": "${env:HOME}/.firefly/stacks/firefly_e2e/stack.json",
+        "STACK_STATE": "${env:HOME}/.firefly/stacks/firefly_e2e/runtime/stackState.json"
       },
       "showLog": true
     },
diff --git a/manifest.json b/manifest.json
index 632b08b55..d6fddb98a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -44,6 +44,6 @@
     "release": "v1.1.0-alpha.1"
   },
   "cli": {
-    "tag": "v1.0.2"
+    "tag": "v1.1.0-alpha.1"
   }
 }


Test patch:
diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go
new file mode 100644
index 000000000..5f3769c99
--- /dev/null
+++ b/test/e2e/e2e.go
@@ -0,0 +1,248 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package e2e
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/go-resty/resty/v2"
+	"github.com/gorilla/websocket"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type TestState interface {
+	T() *testing.T
+	StartTime() time.Time
+	Done() func()
+}
+
+var WidgetSchemaJSON = []byte(`{
+	"$id": "https://example.com/widget.schema.json",
+	"$schema": "https://json-schema.org/draft/2020-12/schema",
+	"title": "Widget",
+	"type": "object",
+	"properties": {
+		"id": {
+			"type": "string",
+			"description": "The unique identifier for the widget."
+		},
+		"name": {
+			"type": "string",
+			"description": "The person's last name."
+		}
+	},
+	"additionalProperties": false
+}`)
+
+func PollForUp(t *testing.T, client *resty.Client) {
+	var resp *resty.Response
+	var err error
+	for i := 0; i < 3; i++ {
+		resp, err = GetNamespaces(client)
+		if err == nil && resp.StatusCode() == 200 {
+			break
+		}
+		time.Sleep(5 * time.Second)
+	}
+	require.NoError(t, err)
+	assert.Equal(t, 200, resp.StatusCode())
+}
+
+func ValidateAccountBalances(t *testing.T, client *resty.Client, poolID *fftypes.UUID, tokenIndex string, balances map[string]int64) {
+	for key, balance := range balances {
+		account := GetTokenBalance(t, client, poolID, tokenIndex, key)
+		assert.Equal(t, balance, account.Balance.Int().Int64())
+	}
+}
+
+func PickTopic(i int, options []string) string {
+	return options[i%len(options)]
+}
+
+func ReadStack(t *testing.T) *Stack {
+	stackFile := os.Getenv("STACK_FILE")
+	if stackFile == "" {
+		t.Fatal("STACK_FILE must be set")
+	}
+	stack, err := ReadStackFile(stackFile)
+	assert.NoError(t, err)
+	return stack
+}
+
+func ReadStackState(t *testing.T) *StackState {
+	stackFile := os.Getenv("STACK_STATE")
+	if stackFile == "" {
+		t.Fatal("STACK_STATE must be set")
+	}
+	stackState, err := ReadStackStateFile(stackFile)
+	assert.NoError(t, err)
+	return stackState
+}
+
+func WsReader(conn *websocket.Conn, dbChanges bool) chan *core.EventDelivery {
+	events := make(chan *core.EventDelivery, 100)
+	go func() {
+		for {
+			_, b, err := conn.ReadMessage()
+			if err != nil {
+				fmt.Printf("Websocket %s closing, error: %s\n", conn.RemoteAddr(), err)
+				return
+			}
+			var wsa core.WSActionBase
+			err = json.Unmarshal(b, &wsa)
+			if err != nil {
+				panic(fmt.Errorf("invalid JSON received on WebSocket: %s", err))
+			}
+			var ed core.EventDelivery
+			err = json.Unmarshal(b, &ed)
+			if err != nil {
+				panic(fmt.Errorf("invalid JSON received on WebSocket: %s", err))
+			}
+			if err == nil {
+				fmt.Printf("Websocket %s event: %s/%s/%s -> %s (tx=%s)\n", conn.RemoteAddr(), ed.Namespace, ed.Type, ed.ID, ed.Reference, ed.Transaction)
+				events <- &ed
+			}
+		}
+	}()
+	return events
+}
+
+func WaitForEvent(t *testing.T, c chan *core.EventDelivery, eventType core.EventType, ref *fftypes.UUID) {
+	for {
+		ed := <-c
+		if ed.Type == eventType && (ref == nil || *ref == *ed.Reference) {
+			t.Logf("Detected '%s' event for ref '%s'", ed.Type, ed.Reference)
+			return
+		}
+		t.Logf("Ignored event '%s'", ed.ID)
+	}
+}
+
+func WaitForMessageConfirmed(t *testing.T, c chan *core.EventDelivery, msgType core.MessageType) *core.EventDelivery {
+	for {
+		ed := <-c
+		if ed.Type == core.EventTypeMessageConfirmed && ed.Message != nil && ed.Message.Header.Type == msgType {
+			t.Logf("Detected '%s' event for message '%s' of type '%s'", ed.Type, ed.Message.Header.ID, msgType)
+			return ed
+		}
+		t.Logf("Ignored event '%s'", ed.ID)
+	}
+}
+
+func WaitForIdentityConfirmed(t *testing.T, c chan *core.EventDelivery) *core.EventDelivery {
+	for {
+		ed := <-c
+		if ed.Type == core.EventTypeIdentityConfirmed {
+			t.Logf("Detected '%s' event for identity '%s'", ed.Type, ed.Reference)
+			return ed
+		}
+		t.Logf("Ignored event '%s'", ed.ID)
+	}
+}
+
+func WaitForContractEvent(t *testing.T, client *resty.Client, c chan *core.EventDelivery, match map[string]interface{}) map[string]interface{} {
+	for {
+		eventDelivery := <-c
+		if eventDelivery.Type == core.EventTypeBlockchainEventReceived {
+			event, err := GetBlockchainEvent(t, client, eventDelivery.Event.Reference.String())
+			if err != nil {
+				t.Logf("WARN: unable to get event: %v", err.Error())
+				continue
+			}
+			eventJSON, ok := event.(map[string]interface{})
+			if !ok {
+				t.Logf("WARN: unable to parse changeEvent: %v", event)
+				continue
+			}
+			if checkObject(t, match, eventJSON) {
+				return eventJSON
+			}
+		}
+	}
+}
+
+func checkObject(t *testing.T, expected interface{}, actual interface{}) bool {
+	match := true
+
+	// check if this is a nested object
+	expectedObject, expectedIsObject := expected.(map[string]interface{})
+	actualObject, actualIsObject := actual.(map[string]interface{})
+
+	t.Logf("Matching blockchain event: %s", fftypes.JSONObject(actualObject).String())
+
+	// check if this is an array
+	expectedArray, expectedIsArray := expected.([]interface{})
+	actualArray, actualIsArray := actual.([]interface{})
+	switch {
+	case expectedIsObject && actualIsObject:
+		for expectedKey, expectedValue := range expectedObject {
+			if !checkObject(t, expectedValue, actualObject[expectedKey]) {
+				return false
+			}
+		}
+	case expectedIsArray && actualIsArray:
+		for _, expectedItem := range expectedArray {
+			for j, actualItem := range actualArray {
+				if checkObject(t, expectedItem, actualItem) {
+					break
+				}
+				if j == len(actualArray)-1 {
+					return false
+				}
+			}
+		}
+	default:
+		expectedString, expectedIsString := expected.(string)
+		actualString, actualIsString := actual.(string)
+		if expectedIsString && actualIsString {
+			return strings.EqualFold(expectedString, actualString)
+		}
+		return expected == actual
+	}
+	return match
+}
+
+func VerifyAllOperationsSucceeded(t *testing.T, clients []*resty.Client, startTime time.Time) {
+	tries := 3
+	delay := 2 * time.Second
+
+	var pending string
+	for i := 0; i < tries; i++ {
+		pending = ""
+		for _, client := range clients {
+			for _, op := range GetOperations(t, client, startTime) {
+				if op.Status != core.OpStatusSucceeded {
+					pending += fmt.Sprintf("Operation '%s' (%s) on '%s' is not successful\n", op.ID, op.Type, client.BaseURL)
+				}
+			}
+		}
+		if pending == "" {
+			return
+		}
+		time.Sleep(delay)
+	}
+
+	assert.Fail(t, pending)
+}
diff --git a/test/e2e/gateway/ethereum_contract_test.go b/test/e2e/gateway/ethereum_contract_test.go
new file mode 100644
index 000000000..e00349ce4
--- /dev/null
+++ b/test/e2e/gateway/ethereum_contract_test.go
@@ -0,0 +1,311 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gateway
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/aidarkhanov/nanoid"
+	"github.com/go-resty/resty/v2"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+)
+
+var contractVersion, _ = nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", nanoid.DefaultSize)
+
+type uploadABIResult struct {
+	ID string `json:"id"`
+}
+
+type deployABIResult struct {
+	ContractAddress string `json:"contractAddress"`
+}
+
+type ethconnectOutput struct {
+	Output string `json:"output"`
+}
+
+type simpleStorageBody struct {
+	NewValue string `json:"newValue"`
+}
+
+func simpleStorageFFIChanged() *fftypes.FFIEvent {
+	return &fftypes.FFIEvent{
+		FFIEventDefinition: fftypes.FFIEventDefinition{
+			Name: "Changed",
+			Params: fftypes.FFIParams{
+				{
+					Name:   "_from",
+					Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "address", "indexed": true}}`),
+				},
+				{
+					Name:   "_value",
+					Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`),
+				},
+			},
+		},
+	}
+}
+
+func simpleStorageFFI() *fftypes.FFI {
+	return &fftypes.FFI{
+		Name:    "SimpleStorage",
+		Version: contractVersion,
+		Methods: []*fftypes.FFIMethod{
+			simpleStorageFFISet(),
+			simpleStorageFFIGet(),
+		},
+		Events: []*fftypes.FFIEvent{
+			simpleStorageFFIChanged(),
+		},
+	}
+}
+
+func simpleStorageFFISet() *fftypes.FFIMethod {
+	return &fftypes.FFIMethod{
+		Name: "set",
+		Params: fftypes.FFIParams{
+			{
+				Name:   "newValue",
+				Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`),
+			},
+		},
+		Returns: fftypes.FFIParams{},
+	}
+}
+
+func simpleStorageFFIGet() *fftypes.FFIMethod {
+	return &fftypes.FFIMethod{
+		Name:   "get",
+		Params: fftypes.FFIParams{},
+		Returns: fftypes.FFIParams{
+			{
+				Name:   "output",
+				Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`),
+			},
+		},
+	}
+}
+
+type EthereumContractTestSuite struct {
+	suite.Suite
+	testState       *testState
+	contractAddress string
+	interfaceID     *fftypes.UUID
+	ethClient       *resty.Client
+	ethIdentity     string
+}
+
+func (suite *EthereumContractTestSuite) SetupSuite() {
+	suite.testState = beforeE2ETest(suite.T())
+	stack := e2e.ReadStack(suite.T())
+	stackState := e2e.ReadStackState(suite.T())
+	suite.ethClient = e2e.NewResty(suite.T())
+	suite.ethClient.SetBaseURL(fmt.Sprintf("http://localhost:%d", stack.Members[0].ExposedConnectorPort))
+	account := stackState.Accounts[0].(map[string]interface{})
+	suite.ethIdentity = account["address"].(string)
+	suite.contractAddress = os.Getenv("CONTRACT_ADDRESS")
+	if suite.contractAddress == "" {
+		suite.T().Fatal("CONTRACT_ADDRESS must be set")
+	}
+	suite.T().Logf("contractAddress: %s", suite.contractAddress)
+
+	res, err := e2e.CreateFFI(suite.T(), suite.testState.client1, simpleStorageFFI())
+	suite.interfaceID = fftypes.MustParseUUID(res.(map[string]interface{})["id"].(string))
+	suite.T().Logf("interfaceID: %s", suite.interfaceID)
+	assert.NoError(suite.T(), err)
+}
+
+func (suite *EthereumContractTestSuite) BeforeTest(suiteName, testName string) {
+	suite.testState = beforeE2ETest(suite.T())
+}
+
+func (suite *EthereumContractTestSuite) AfterTest(suiteName, testName string) {
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1}, suite.testState.startTime)
+}
+
+func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
+	defer suite.testState.Done()
+
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	listener := e2e.CreateContractListener(suite.T(), suite.testState.client1, simpleStorageFFIChanged(), &fftypes.JSONObject{
+		"address": suite.contractAddress,
+	})
+
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 1, len(listeners))
+	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
+
+	location := map[string]interface{}{
+		"address": suite.contractAddress,
+	}
+	locationBytes, _ := json.Marshal(location)
+	invokeContractRequest := &core.ContractCallRequest{
+		Location: fftypes.JSONAnyPtrBytes(locationBytes),
+		Method:   simpleStorageFFISet(),
+		Input: map[string]interface{}{
+			"newValue": float64(2),
+		},
+	}
+
+	res, err := e2e.InvokeContractMethod(suite.T(), suite.testState.client1, invokeContractRequest)
+	assert.NoError(suite.T(), err)
+	assert.NotNil(suite.T(), res)
+
+	match := map[string]interface{}{
+		"info": map[string]interface{}{
+			"address": suite.contractAddress,
+		},
+		"output": map[string]interface{}{
+			"_value": "2",
+			"_from":  suite.ethIdentity,
+		},
+		"listener": listener.ID.String(),
+	}
+
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	assert.NotNil(suite.T(), event)
+
+	queryContractRequest := &core.ContractCallRequest{
+		Location: fftypes.JSONAnyPtrBytes(locationBytes),
+		Method:   simpleStorageFFIGet(),
+	}
+	res, err = e2e.QueryContractMethod(suite.T(), suite.testState.client1, queryContractRequest)
+	assert.NoError(suite.T(), err)
+	resJSON, err := json.Marshal(res)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"2"}`, string(resJSON))
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+}
+
+func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
+	defer suite.testState.Done()
+
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+
+	ffiReference := &fftypes.FFIReference{
+		ID: suite.interfaceID,
+	}
+	listener := e2e.CreateFFIContractListener(suite.T(), suite.testState.client1, ffiReference, "Changed", &fftypes.JSONObject{
+		"address": suite.contractAddress,
+	})
+
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 1, len(listeners))
+	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
+
+	location := map[string]interface{}{
+		"address": suite.contractAddress,
+	}
+	locationBytes, _ := json.Marshal(location)
+	invokeContractRequest := &core.ContractCallRequest{
+		Location: fftypes.JSONAnyPtrBytes(locationBytes),
+		Input: map[string]interface{}{
+			"newValue": float64(42),
+		},
+		Interface:  suite.interfaceID,
+		MethodPath: "set",
+	}
+
+	res, err := e2e.InvokeContractMethod(suite.T(), suite.testState.client1, invokeContractRequest)
+	assert.NoError(suite.T(), err)
+	assert.NotNil(suite.T(), res)
+
+	match := map[string]interface{}{
+		"info": map[string]interface{}{
+			"address": suite.contractAddress,
+		},
+		"output": map[string]interface{}{
+			"_value": "42",
+			"_from":  suite.ethIdentity,
+		},
+		"listener": listener.ID.String(),
+	}
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	assert.NotNil(suite.T(), event)
+
+	queryContractRequest := &core.ContractCallRequest{
+		Location:   fftypes.JSONAnyPtrBytes(locationBytes),
+		Interface:  suite.interfaceID,
+		MethodPath: "get",
+	}
+	res, err = e2e.QueryContractMethod(suite.T(), suite.testState.client1, queryContractRequest)
+	assert.NoError(suite.T(), err)
+	resJSON, err := json.Marshal(res)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"42"}`, string(resJSON))
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+}
+
+func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
+	defer suite.testState.Done()
+
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	APIName := fftypes.NewUUID().String()
+
+	ffiReference := &fftypes.FFIReference{
+		ID: suite.interfaceID,
+	}
+
+	location := map[string]interface{}{
+		"address": suite.contractAddress,
+	}
+	locationBytes, _ := json.Marshal(location)
+
+	createContractAPIResult, err := e2e.CreateContractAPI(suite.T(), suite.testState.client1, APIName, ffiReference, fftypes.JSONAnyPtr(string(locationBytes)))
+	assert.NotNil(suite.T(), createContractAPIResult)
+	assert.NoError(suite.T(), err)
+
+	listener, err := e2e.CreateContractAPIListener(suite.T(), suite.testState.client1, APIName, "Changed", "firefly_e2e")
+	assert.NoError(suite.T(), err)
+
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 1, len(listeners))
+	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
+
+	input := fftypes.JSONAny(`{"newValue": 42}`)
+	invokeResult, err := e2e.InvokeContractAPIMethod(suite.T(), suite.testState.client1, APIName, "set", &input)
+	assert.NoError(suite.T(), err)
+	assert.NotNil(suite.T(), invokeResult)
+
+	match := map[string]interface{}{
+		"info": map[string]interface{}{
+			"address": suite.contractAddress,
+		},
+		"output": map[string]interface{}{
+			"_value": "42",
+			"_from":  suite.ethIdentity,
+		},
+		"listener": listener.ID.String(),
+	}
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	assert.NotNil(suite.T(), event)
+	assert.NoError(suite.T(), err)
+
+	res, err := e2e.QueryContractAPIMethod(suite.T(), suite.testState.client1, APIName, "get", fftypes.JSONAnyPtr("{}"))
+	assert.NoError(suite.T(), err)
+	resJSON, err := json.Marshal(res)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"42"}`, string(resJSON))
+
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+}
diff --git a/test/e2e/gateway/ethereum_test.go b/test/e2e/gateway/ethereum_test.go
new file mode 100644
index 000000000..7ce541258
--- /dev/null
+++ b/test/e2e/gateway/ethereum_test.go
@@ -0,0 +1,28 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gateway
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/suite"
+)
+
+func TestEthereumGatewayE2ESuite(t *testing.T) {
+	suite.Run(t, new(TokensTestSuite))
+	suite.Run(t, new(EthereumContractTestSuite))
+}
diff --git a/test/e2e/fabric_contract_test.go b/test/e2e/gateway/fabric_contract_test.go
similarity index 81%
rename from test/e2e/fabric_contract_test.go
rename to test/e2e/gateway/fabric_contract_test.go
index 366035783..ff2c5d85e 100644
--- a/test/e2e/fabric_contract_test.go
+++ b/test/e2e/gateway/fabric_contract_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package gateway
 
 import (
 	"encoding/json"
@@ -27,6 +27,7 @@ import (
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
@@ -87,7 +88,7 @@ func deployChaincode(t *testing.T, stackName string) string {
 	assert.NoError(t, err)
 	chaincodeName := "e2e_" + id
 
-	cmd := exec.Command("bash", "./deploy_chaincode.sh")
+	cmd := exec.Command("bash", "../deploy_chaincode.sh")
 	cmd.Env = append(cmd.Env, "STACK_NAME="+stackName)
 	cmd.Env = append(cmd.Env, "CHAINCODE_NAME="+chaincodeName)
 	cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
@@ -127,10 +128,10 @@ type FabricContractTestSuite struct {
 }
 
 func (suite *FabricContractTestSuite) SetupSuite() {
-	stack := readStackFile(suite.T())
+	stack := e2e.ReadStack(suite.T())
 	suite.chaincodeName = deployChaincode(suite.T(), stack.Name)
 
-	suite.fabClient = NewResty(suite.T())
+	suite.fabClient = e2e.NewResty(suite.T())
 	suite.fabClient.SetBaseURL(fmt.Sprintf("http://localhost:%d", stack.Members[0].ExposedConnectorPort))
 }
 
@@ -139,20 +140,20 @@ func (suite *FabricContractTestSuite) BeforeTest(suiteName, testName string) {
 }
 
 func (suite *FabricContractTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1}, suite.testState.startTime)
 }
 
 func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
 
-	sub := CreateContractListener(suite.T(), suite.testState.client1, assetCreatedEvent, &fftypes.JSONObject{
+	sub := e2e.CreateContractListener(suite.T(), suite.testState.client1, assetCreatedEvent, &fftypes.JSONObject{
 		"channel":   "firefly",
 		"chaincode": suite.chaincodeName,
 	})
 
-	subs := GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	subs := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(subs))
 	assert.Equal(suite.T(), sub.BackendID, subs[0].BackendID)
 
@@ -170,13 +171,13 @@ func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 		},
 	}
 
-	res, err := InvokeContractMethod(suite.testState.t, suite.testState.client1, invokeContractRequest)
+	res, err := e2e.InvokeContractMethod(suite.testState.t, suite.testState.client1, invokeContractRequest)
 	suite.T().Log(res)
 	assert.NoError(suite.T(), err)
 
 	<-received1
 
-	events := GetContractEvents(suite.T(), suite.testState.client1, suite.testState.startTime, sub.ID)
+	events := e2e.GetContractEvents(suite.T(), suite.testState.client1, suite.testState.startTime, sub.ID)
 	assert.Equal(suite.T(), 1, len(events))
 	assert.Equal(suite.T(), "AssetCreated", events[0].Name)
 	assert.Equal(suite.T(), assetName, events[0].Output.GetString("name"))
@@ -189,13 +190,13 @@ func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 		},
 	}
 
-	res, err = QueryContractMethod(suite.testState.t, suite.testState.client1, queryContractRequest)
+	res, err = e2e.QueryContractMethod(suite.testState.t, suite.testState.client1, queryContractRequest)
 	suite.T().Log(res)
 	assert.NoError(suite.T(), err)
 	assert.Equal(suite.T(), assetName, res.(map[string]interface{})["name"])
 
-	DeleteContractListener(suite.T(), suite.testState.client1, subs[0].ID)
-	subs = GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, subs[0].ID)
+	subs = e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 0, len(subs))
 
 }
diff --git a/test/e2e/gateway/fabric_test.go b/test/e2e/gateway/fabric_test.go
new file mode 100644
index 000000000..0021278b5
--- /dev/null
+++ b/test/e2e/gateway/fabric_test.go
@@ -0,0 +1,27 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gateway
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/suite"
+)
+
+func TestFabricGatewayE2ESuite(t *testing.T) {
+	suite.Run(t, new(FabricContractTestSuite))
+}
diff --git a/test/e2e/gateway/gateway_test.go b/test/e2e/gateway/gateway_test.go
new file mode 100644
index 000000000..81143efb9
--- /dev/null
+++ b/test/e2e/gateway/gateway_test.go
@@ -0,0 +1,128 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gateway
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"net/url"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/go-resty/resty/v2"
+	"github.com/gorilla/websocket"
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/stretchr/testify/require"
+)
+
+type testState struct {
+	t                    *testing.T
+	startTime            time.Time
+	done                 func()
+	ws1                  *websocket.Conn
+	client1              *resty.Client
+	unregisteredAccounts []interface{}
+	namespace            string
+}
+
+func (m *testState) T() *testing.T {
+	return m.t
+}
+
+func (m *testState) StartTime() time.Time {
+	return m.startTime
+}
+
+func (m *testState) Done() func() {
+	return m.done
+}
+
+func beforeE2ETest(t *testing.T) *testState {
+	stack := e2e.ReadStack(t)
+	stackState := e2e.ReadStackState(t)
+	namespace := "default"
+
+	var authHeader1 http.Header
+
+	ts := &testState{
+		t:                    t,
+		startTime:            time.Now(),
+		client1:              e2e.NewResty(t),
+		unregisteredAccounts: stackState.Accounts[2:],
+		namespace:            namespace,
+	}
+
+	httpProtocolClient1 := "http"
+	websocketProtocolClient1 := "ws"
+	if stack.Members[0].UseHTTPS {
+		httpProtocolClient1 = "https"
+		websocketProtocolClient1 = "wss"
+	}
+
+	member0WithPort := ""
+	if stack.Members[0].ExposedFireflyPort != 0 {
+		member0WithPort = fmt.Sprintf(":%d", stack.Members[0].ExposedFireflyPort)
+	}
+
+	ts.client1.SetBaseURL(fmt.Sprintf("%s://%s%s/api/v1", httpProtocolClient1, stack.Members[0].FireflyHostname, member0WithPort))
+
+	t.Logf("Blockchain provider: %s", stack.BlockchainProvider)
+
+	if stack.Members[0].Username != "" && stack.Members[0].Password != "" {
+		t.Log("Setting auth for user 1")
+		ts.client1.SetBasicAuth(stack.Members[0].Username, stack.Members[0].Password)
+		authHeader1 = http.Header{
+			"Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", stack.Members[0].Username, stack.Members[0].Password))))},
+		}
+	}
+
+	// If no namespace is set to run tests against, use the default namespace
+	if os.Getenv("NAMESPACE") != "" {
+		namespace := os.Getenv("NAMESPACE")
+		ts.namespace = namespace
+	}
+
+	t.Logf("Client 1: " + ts.client1.HostURL)
+	e2e.PollForUp(t, ts.client1)
+
+	eventNames := "message_confirmed|token_pool_confirmed|token_transfer_confirmed|blockchain_event_received|token_approval_confirmed|identity_confirmed"
+	queryString := fmt.Sprintf("namespace=%s&ephemeral&autoack&filter.events=%s&changeevents=.*", ts.namespace, eventNames)
+
+	wsUrl1 := url.URL{
+		Scheme:   websocketProtocolClient1,
+		Host:     fmt.Sprintf("%s%s", stack.Members[0].FireflyHostname, member0WithPort),
+		Path:     "/ws",
+		RawQuery: queryString,
+	}
+
+	t.Logf("Websocket 1: " + wsUrl1.String())
+
+	var err error
+	ts.ws1, _, err = websocket.DefaultDialer.Dial(wsUrl1.String(), authHeader1)
+	if err != nil {
+		t.Logf(err.Error())
+	}
+	require.NoError(t, err)
+
+	ts.done = func() {
+		ts.ws1.Close()
+		t.Log("WebSockets closed")
+	}
+	return ts
+}
diff --git a/test/e2e/gateway/tokens_test.go b/test/e2e/gateway/tokens_test.go
new file mode 100644
index 000000000..d5c89cf8c
--- /dev/null
+++ b/test/e2e/gateway/tokens_test.go
@@ -0,0 +1,103 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gateway
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+
+	"github.com/go-resty/resty/v2"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+)
+
+type TokensTestSuite struct {
+	suite.Suite
+	testState *testState
+	connector string
+	key       string
+}
+
+func (suite *TokensTestSuite) SetupSuite() {
+	suite.testState = beforeE2ETest(suite.T())
+	stack := e2e.ReadStack(suite.T())
+	stackState := e2e.ReadStackState(suite.T())
+	suite.connector = stack.TokenProviders[0]
+	account := stackState.Accounts[0].(map[string]interface{})
+	suite.key = account["address"].(string)
+}
+
+func (suite *TokensTestSuite) BeforeTest(suiteName, testName string) {
+	suite.testState = beforeE2ETest(suite.T())
+}
+
+func (suite *TokensTestSuite) AfterTest(suiteName, testName string) {
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1}, suite.testState.startTime)
+}
+
+func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
+	defer suite.testState.done()
+
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+
+	pools := e2e.GetTokenPools(suite.T(), suite.testState.client1, time.Unix(0, 0))
+	rand.Seed(time.Now().UnixNano())
+	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
+	suite.T().Logf("Pool name: %s", poolName)
+
+	pool := &core.TokenPool{
+		Name:   poolName,
+		Type:   core.TokenTypeFungible,
+		Config: fftypes.JSONObject{},
+	}
+
+	poolResp := e2e.CreateTokenPool(suite.T(), suite.testState.client1, pool, false)
+	poolID := poolResp.ID
+
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
+	pools = e2e.GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 1, len(pools))
+	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
+	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
+	assert.Equal(suite.T(), poolName, pools[0].Name)
+	assert.Equal(suite.T(), core.TokenTypeFungible, pools[0].Type)
+	assert.NotEmpty(suite.T(), pools[0].Locator)
+
+	transfer := &core.TokenTransferInput{
+		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1)},
+		Pool:          poolName,
+	}
+	transferOut := e2e.MintTokens(suite.T(), suite.testState.client1, transfer, false)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
+		suite.key: 1,
+	})
+
+	transfer = &core.TokenTransferInput{
+		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1)},
+		Pool:          poolName,
+	}
+	transferOut = e2e.BurnTokens(suite.T(), suite.testState.client1, transfer, false)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
+		suite.key: 0,
+	})
+}
diff --git a/test/e2e/ethereum_contract_test.go b/test/e2e/multiparty/ethereum_contract_test.go
similarity index 69%
rename from test/e2e/ethereum_contract_test.go
rename to test/e2e/multiparty/ethereum_contract_test.go
index b5d7db5c5..7954eac38 100644
--- a/test/e2e/ethereum_contract_test.go
+++ b/test/e2e/multiparty/ethereum_contract_test.go
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"encoding/json"
@@ -25,6 +25,7 @@ import (
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 )
@@ -116,8 +117,8 @@ type EthereumContractTestSuite struct {
 
 func (suite *EthereumContractTestSuite) SetupSuite() {
 	suite.testState = beforeE2ETest(suite.T())
-	stack := readStackFile(suite.T())
-	suite.ethClient = NewResty(suite.T())
+	stack := e2e.ReadStack(suite.T())
+	suite.ethClient = e2e.NewResty(suite.T())
 	suite.ethClient.SetBaseURL(fmt.Sprintf("http://localhost:%d", stack.Members[0].ExposedConnectorPort))
 	suite.ethIdentity = suite.testState.org1key.Value
 	suite.contractAddress = os.Getenv("CONTRACT_ADDRESS")
@@ -126,7 +127,7 @@ func (suite *EthereumContractTestSuite) SetupSuite() {
 	}
 	suite.T().Logf("contractAddress: %s", suite.contractAddress)
 
-	res, err := CreateFFI(suite.T(), suite.testState.client1, simpleStorageFFI())
+	res, err := e2e.CreateFFI(suite.T(), suite.testState.client1, simpleStorageFFI())
 	suite.interfaceID = fftypes.MustParseUUID(res.(map[string]interface{})["id"].(string))
 	suite.T().Logf("interfaceID: %s", suite.interfaceID)
 	assert.NoError(suite.T(), err)
@@ -137,18 +138,18 @@ func (suite *EthereumContractTestSuite) BeforeTest(suiteName, testName string) {
 }
 
 func (suite *EthereumContractTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
 }
 
 func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
-	defer suite.testState.done()
+	defer suite.testState.Done()
 
-	received1 := wsReader(suite.testState.ws1, true)
-	listener := CreateContractListener(suite.T(), suite.testState.client1, simpleStorageFFIChanged(), &fftypes.JSONObject{
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	listener := e2e.CreateContractListener(suite.T(), suite.testState.client1, simpleStorageFFIChanged(), &fftypes.JSONObject{
 		"address": suite.contractAddress,
 	})
 
-	listeners := GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(listeners))
 	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
 
@@ -164,7 +165,7 @@ func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 		},
 	}
 
-	res, err := InvokeContractMethod(suite.testState.t, suite.testState.client1, invokeContractRequest)
+	res, err := e2e.InvokeContractMethod(suite.T(), suite.testState.client1, invokeContractRequest)
 	assert.NoError(suite.T(), err)
 	assert.NotNil(suite.T(), res)
 
@@ -179,34 +180,34 @@ func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 		"listener": listener.ID.String(),
 	}
 
-	event := waitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
 	assert.NotNil(suite.T(), event)
 
 	queryContractRequest := &core.ContractCallRequest{
 		Location: fftypes.JSONAnyPtrBytes(locationBytes),
 		Method:   simpleStorageFFIGet(),
 	}
-	res, err = QueryContractMethod(suite.testState.t, suite.testState.client1, queryContractRequest)
-	assert.NoError(suite.testState.t, err)
+	res, err = e2e.QueryContractMethod(suite.T(), suite.testState.client1, queryContractRequest)
+	assert.NoError(suite.T(), err)
 	resJSON, err := json.Marshal(res)
-	assert.NoError(suite.testState.t, err)
-	assert.Equal(suite.testState.t, `{"output":"2"}`, string(resJSON))
-	DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"2"}`, string(resJSON))
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
 }
 
 func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
-	defer suite.testState.done()
+	defer suite.testState.Done()
 
-	received1 := wsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
 
 	ffiReference := &fftypes.FFIReference{
 		ID: suite.interfaceID,
 	}
-	listener := CreateFFIContractListener(suite.T(), suite.testState.client1, ffiReference, "Changed", &fftypes.JSONObject{
+	listener := e2e.CreateFFIContractListener(suite.T(), suite.testState.client1, ffiReference, "Changed", &fftypes.JSONObject{
 		"address": suite.contractAddress,
 	})
 
-	listeners := GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(listeners))
 	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
 
@@ -223,9 +224,9 @@ func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 		MethodPath: "set",
 	}
 
-	res, err := InvokeContractMethod(suite.testState.t, suite.testState.client1, invokeContractRequest)
-	assert.NoError(suite.testState.t, err)
-	assert.NotNil(suite.testState.t, res)
+	res, err := e2e.InvokeContractMethod(suite.T(), suite.testState.client1, invokeContractRequest)
+	assert.NoError(suite.T(), err)
+	assert.NotNil(suite.T(), res)
 
 	match := map[string]interface{}{
 		"info": map[string]interface{}{
@@ -237,7 +238,7 @@ func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 		},
 		"listener": listener.ID.String(),
 	}
-	event := waitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
 	assert.NotNil(suite.T(), event)
 
 	queryContractRequest := &core.ContractCallRequest{
@@ -245,18 +246,18 @@ func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 		Interface:  suite.interfaceID,
 		MethodPath: "get",
 	}
-	res, err = QueryContractMethod(suite.testState.t, suite.testState.client1, queryContractRequest)
-	assert.NoError(suite.testState.t, err)
+	res, err = e2e.QueryContractMethod(suite.T(), suite.testState.client1, queryContractRequest)
+	assert.NoError(suite.T(), err)
 	resJSON, err := json.Marshal(res)
-	assert.NoError(suite.testState.t, err)
-	assert.Equal(suite.testState.t, `{"output":"42"}`, string(resJSON))
-	DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"42"}`, string(resJSON))
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
 }
 
 func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
-	defer suite.testState.done()
+	defer suite.testState.Done()
 
-	received1 := wsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
 	APIName := fftypes.NewUUID().String()
 
 	ffiReference := &fftypes.FFIReference{
@@ -268,20 +269,20 @@ func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
 	}
 	locationBytes, _ := json.Marshal(location)
 
-	createContractAPIResult, err := CreateContractAPI(suite.T(), suite.testState.client1, APIName, ffiReference, fftypes.JSONAnyPtr(string(locationBytes)))
+	createContractAPIResult, err := e2e.CreateContractAPI(suite.T(), suite.testState.client1, APIName, ffiReference, fftypes.JSONAnyPtr(string(locationBytes)))
 	assert.NotNil(suite.T(), createContractAPIResult)
 	assert.NoError(suite.T(), err)
 
-	listener, err := CreateContractAPIListener(suite.T(), suite.testState.client1, APIName, "Changed", "firefly_e2e")
+	listener, err := e2e.CreateContractAPIListener(suite.T(), suite.testState.client1, APIName, "Changed", "firefly_e2e")
 	assert.NoError(suite.T(), err)
 
-	listeners := GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	listeners := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(listeners))
 	assert.Equal(suite.T(), listener.BackendID, listeners[0].BackendID)
 
 	input := fftypes.JSONAny(`{"newValue": 42}`)
-	invokeResult, err := InvokeContractAPIMethod(suite.T(), suite.testState.client1, APIName, "set", &input)
-	assert.NoError(suite.testState.t, err)
+	invokeResult, err := e2e.InvokeContractAPIMethod(suite.T(), suite.testState.client1, APIName, "set", &input)
+	assert.NoError(suite.T(), err)
 	assert.NotNil(suite.T(), invokeResult)
 
 	match := map[string]interface{}{
@@ -294,15 +295,15 @@ func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
 		},
 		"listener": listener.ID.String(),
 	}
-	event := waitForContractEvent(suite.T(), suite.testState.client1, received1, match)
+	event := e2e.WaitForContractEvent(suite.T(), suite.testState.client1, received1, match)
 	assert.NotNil(suite.T(), event)
-	assert.NoError(suite.testState.t, err)
+	assert.NoError(suite.T(), err)
 
-	res, err := QueryContractAPIMethod(suite.T(), suite.testState.client1, APIName, "get", fftypes.JSONAnyPtr("{}"))
-	assert.NoError(suite.testState.t, err)
+	res, err := e2e.QueryContractAPIMethod(suite.T(), suite.testState.client1, APIName, "get", fftypes.JSONAnyPtr("{}"))
+	assert.NoError(suite.T(), err)
 	resJSON, err := json.Marshal(res)
-	assert.NoError(suite.testState.t, err)
-	assert.Equal(suite.testState.t, `{"output":"42"}`, string(resJSON))
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), `{"output":"42"}`, string(resJSON))
 
-	DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, listener.ID)
 }
diff --git a/test/e2e/ethereum_namespace_test.go b/test/e2e/multiparty/ethereum_namespace_test.go
similarity index 93%
rename from test/e2e/ethereum_namespace_test.go
rename to test/e2e/multiparty/ethereum_namespace_test.go
index da54f4fad..095251ee8 100644
--- a/test/e2e/ethereum_namespace_test.go
+++ b/test/e2e/multiparty/ethereum_namespace_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"testing"
diff --git a/test/e2e/ethereum_test.go b/test/e2e/multiparty/ethereum_test.go
similarity index 88%
rename from test/e2e/ethereum_test.go
rename to test/e2e/multiparty/ethereum_test.go
index 5e54dc617..6aa380d8a 100644
--- a/test/e2e/ethereum_test.go
+++ b/test/e2e/multiparty/ethereum_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"testing"
@@ -22,7 +22,7 @@ import (
 	"github.com/stretchr/testify/suite"
 )
 
-func TestEthereumE2ESuite(t *testing.T) {
+func TestEthereumMultipartyE2ESuite(t *testing.T) {
 	suite.Run(t, new(IdentityTestSuite))
 	suite.Run(t, new(OnChainOffChainTestSuite))
 	suite.Run(t, new(TokensTestSuite))
diff --git a/test/e2e/multiparty/fabric_contract_test.go b/test/e2e/multiparty/fabric_contract_test.go
new file mode 100644
index 000000000..35e817bec
--- /dev/null
+++ b/test/e2e/multiparty/fabric_contract_test.go
@@ -0,0 +1,202 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package multiparty
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"testing"
+
+	"github.com/aidarkhanov/nanoid"
+	"github.com/go-resty/resty/v2"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+)
+
+type fabConnectHeaders struct {
+	Type      string `json:"type"`
+	Channel   string `json:"channel"`
+	Signer    string `json:"signer"`
+	Chaincode string `json:"chaincode"`
+}
+
+type createAssetBody struct {
+	Headers fabConnectHeaders `json:"headers"`
+	Func    string            `json:"func"`
+	Args    []string          `json:"args"`
+}
+
+var assetCreatedEvent = &fftypes.FFIEvent{
+	FFIEventDefinition: fftypes.FFIEventDefinition{
+		Name: "AssetCreated",
+	},
+}
+
+func assetManagerCreateAsset() *fftypes.FFIMethod {
+	return &fftypes.FFIMethod{
+		Name: "CreateAsset",
+		Params: fftypes.FFIParams{
+			{
+				Name:   "name",
+				Schema: fftypes.JSONAnyPtr(`{"type": "string"}`),
+			},
+		},
+		Returns: fftypes.FFIParams{},
+	}
+}
+
+func assetManagerGetAsset() *fftypes.FFIMethod {
+	return &fftypes.FFIMethod{
+		Name: "GetAsset",
+		Params: fftypes.FFIParams{
+			{
+				Name:   "name",
+				Schema: fftypes.JSONAnyPtr(`{"type": "string"}`),
+			},
+		},
+		Returns: fftypes.FFIParams{
+			{
+				Name:   "name",
+				Schema: fftypes.JSONAnyPtr(`{"type": "string"}`),
+			},
+		},
+	}
+}
+
+func deployChaincode(t *testing.T, stackName string) string {
+	id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", nanoid.DefaultSize)
+	assert.NoError(t, err)
+	chaincodeName := "e2e_" + id
+
+	cmd := exec.Command("bash", "../deploy_chaincode.sh")
+	cmd.Env = append(cmd.Env, "STACK_NAME="+stackName)
+	cmd.Env = append(cmd.Env, "CHAINCODE_NAME="+chaincodeName)
+	cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err = cmd.Run()
+	require.NoError(t, err)
+
+	return chaincodeName
+}
+
+func invokeFabContract(t *testing.T, client *resty.Client, channel, chaincode, signer, method string, args []string) {
+	path := "/transactions"
+	body := &createAssetBody{
+		Headers: fabConnectHeaders{
+			Type:      "SendTransaction",
+			Channel:   channel,
+			Signer:    signer,
+			Chaincode: chaincode,
+		},
+		Func: method,
+		Args: args,
+	}
+	resp, err := client.R().
+		SetHeader("x-firefly-sync", "true").
+		SetBody(body).
+		Post(path)
+	require.NoError(t, err)
+	require.Equal(t, 200, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
+}
+
+type FabricContractTestSuite struct {
+	suite.Suite
+	testState     *testState
+	chaincodeName string
+	fabClient     *resty.Client
+}
+
+func (suite *FabricContractTestSuite) SetupSuite() {
+	stack := e2e.ReadStack(suite.T())
+	suite.chaincodeName = deployChaincode(suite.T(), stack.Name)
+
+	suite.fabClient = e2e.NewResty(suite.T())
+	suite.fabClient.SetBaseURL(fmt.Sprintf("http://localhost:%d", stack.Members[0].ExposedConnectorPort))
+}
+
+func (suite *FabricContractTestSuite) BeforeTest(suiteName, testName string) {
+	suite.testState = beforeE2ETest(suite.T())
+}
+
+func (suite *FabricContractTestSuite) AfterTest(suiteName, testName string) {
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+}
+
+func (suite *FabricContractTestSuite) TestE2EContractEvents() {
+	defer suite.testState.done()
+
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+
+	sub := e2e.CreateContractListener(suite.T(), suite.testState.client1, assetCreatedEvent, &fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": suite.chaincodeName,
+	})
+
+	subs := e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 1, len(subs))
+	assert.Equal(suite.T(), sub.BackendID, subs[0].BackendID)
+
+	assetName := nanoid.New()
+	location := map[string]interface{}{
+		"chaincode": suite.chaincodeName,
+		"channel":   "firefly",
+	}
+	locationBytes, _ := json.Marshal(location)
+	invokeContractRequest := &core.ContractCallRequest{
+		Location: fftypes.JSONAnyPtrBytes(locationBytes),
+		Method:   assetManagerCreateAsset(),
+		Input: map[string]interface{}{
+			"name": assetName,
+		},
+	}
+
+	res, err := e2e.InvokeContractMethod(suite.testState.t, suite.testState.client1, invokeContractRequest)
+	suite.T().Log(res)
+	assert.NoError(suite.T(), err)
+
+	<-received1
+
+	events := e2e.GetContractEvents(suite.T(), suite.testState.client1, suite.testState.startTime, sub.ID)
+	assert.Equal(suite.T(), 1, len(events))
+	assert.Equal(suite.T(), "AssetCreated", events[0].Name)
+	assert.Equal(suite.T(), assetName, events[0].Output.GetString("name"))
+
+	queryContractRequest := &core.ContractCallRequest{
+		Location: fftypes.JSONAnyPtrBytes(locationBytes),
+		Method:   assetManagerGetAsset(),
+		Input: map[string]interface{}{
+			"name": assetName,
+		},
+	}
+
+	res, err = e2e.QueryContractMethod(suite.testState.t, suite.testState.client1, queryContractRequest)
+	suite.T().Log(res)
+	assert.NoError(suite.T(), err)
+	assert.Equal(suite.T(), assetName, res.(map[string]interface{})["name"])
+
+	e2e.DeleteContractListener(suite.T(), suite.testState.client1, subs[0].ID)
+	subs = e2e.GetContractListeners(suite.T(), suite.testState.client1, suite.testState.startTime)
+	assert.Equal(suite.T(), 0, len(subs))
+
+}
diff --git a/test/e2e/fabric_test.go b/test/e2e/multiparty/fabric_test.go
similarity index 88%
rename from test/e2e/fabric_test.go
rename to test/e2e/multiparty/fabric_test.go
index 9970f651d..dfbf6a932 100644
--- a/test/e2e/fabric_test.go
+++ b/test/e2e/multiparty/fabric_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"testing"
@@ -22,7 +22,7 @@ import (
 	"github.com/stretchr/testify/suite"
 )
 
-func TestFabricE2ESuite(t *testing.T) {
+func TestFabricMultipartyE2ESuite(t *testing.T) {
 	suite.Run(t, new(IdentityTestSuite))
 	suite.Run(t, new(OnChainOffChainTestSuite))
 	suite.Run(t, new(FabricContractTestSuite))
diff --git a/test/e2e/identity_test.go b/test/e2e/multiparty/identity_test.go
similarity index 72%
rename from test/e2e/identity_test.go
rename to test/e2e/multiparty/identity_test.go
index 44fda88cc..38c5ddc94 100644
--- a/test/e2e/identity_test.go
+++ b/test/e2e/multiparty/identity_test.go
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"context"
@@ -25,6 +25,7 @@ import (
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
@@ -40,21 +41,21 @@ func (suite *IdentityTestSuite) BeforeTest(suiteName, testName string) {
 }
 
 func (suite *IdentityTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
 }
 
 func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 	defer suite.testState.done()
 
 	ctx := context.Background()
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	totalIdentities := 2
 	ts := time.Now().Unix()
 	for i := 0; i < totalIdentities; i++ {
 		key := getUnregisteredAccount(suite, suite.testState.org1.Name)
-		ClaimCustomIdentity(suite.T(),
+		e2e.ClaimCustomIdentity(suite.T(),
 			suite.testState.client1,
 			key,
 			fmt.Sprintf("custom_%d_%d", ts, i),
@@ -66,10 +67,10 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 
 	identityIDs := make(map[fftypes.UUID]bool)
 	for i := 0; i < totalIdentities; i++ {
-		ed := waitForIdentityConfirmed(suite.T(), received1)
+		ed := e2e.WaitForIdentityConfirmed(suite.T(), received1)
 		identityIDs[*ed.Reference] = true
 		suite.T().Logf("Received node 1 confirmation of identity %s", ed.Reference)
-		ed = waitForIdentityConfirmed(suite.T(), received2)
+		ed = e2e.WaitForIdentityConfirmed(suite.T(), received2)
 		identityIDs[*ed.Reference] = true
 		suite.T().Logf("Received node 2 confirmation of identity %s", ed.Reference)
 	}
@@ -77,23 +78,23 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 
 	identities := make(map[string]*core.Identity)
 	for identityID := range identityIDs {
-		identityNode1 := GetIdentity(suite.T(), suite.testState.client1, &identityID)
-		identityNode2 := GetIdentity(suite.T(), suite.testState.client1, &identityID)
+		identityNode1 := e2e.GetIdentity(suite.T(), suite.testState.client1, &identityID)
+		identityNode2 := e2e.GetIdentity(suite.T(), suite.testState.client1, &identityID)
 		assert.True(suite.T(), identityNode1.IdentityBase.Equals(ctx, &identityNode2.IdentityBase))
 		identities[identityNode1.DID] = identityNode1
 	}
 
 	// Send a broadcast from each custom identity
 	for did := range identities {
-		resp, err := BroadcastMessageAsIdentity(suite.T(), suite.testState.client1, did, "identitytest", &core.DataRefOrValue{
+		resp, err := e2e.BroadcastMessageAsIdentity(suite.T(), suite.testState.client1, did, "identitytest", &core.DataRefOrValue{
 			Value: fftypes.JSONAnyPtr(`{"some": "data"}`),
 		}, false)
 		require.NoError(suite.T(), err)
 		assert.Equal(suite.T(), 202, resp.StatusCode())
 	}
 	for range identities {
-		waitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
-		waitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+		e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+		e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
 	}
 
 }
@@ -101,14 +102,14 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 func (suite *IdentityTestSuite) TestCustomChildIdentityPrivate() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	org1key := getUnregisteredAccount(suite, suite.testState.org1.Name)
 	org2key := getUnregisteredAccount(suite, suite.testState.org2.Name)
 
 	ts := time.Now().Unix()
-	custom1 := ClaimCustomIdentity(suite.T(),
+	custom1 := e2e.ClaimCustomIdentity(suite.T(),
 		suite.testState.client1,
 		org1key,
 		fmt.Sprintf("custom_%d_org1priv", ts),
@@ -116,7 +117,7 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityPrivate() {
 		nil,
 		suite.testState.org1.ID,
 		true)
-	custom2 := ClaimCustomIdentity(suite.T(),
+	custom2 := e2e.ClaimCustomIdentity(suite.T(),
 		suite.testState.client2,
 		org2key,
 		fmt.Sprintf("custom_%d_org2priv", ts),
@@ -125,25 +126,25 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityPrivate() {
 		suite.testState.org2.ID,
 		true)
 	for i := 0; i < 2; i++ {
-		waitForIdentityConfirmed(suite.T(), received1)
-		waitForIdentityConfirmed(suite.T(), received2)
+		e2e.WaitForIdentityConfirmed(suite.T(), received1)
+		e2e.WaitForIdentityConfirmed(suite.T(), received2)
 	}
 
-	resp, err := PrivateMessageWithKey(suite.testState, suite.testState.client1, org1key, "topic1", &core.DataRefOrValue{
+	resp, err := e2e.PrivateMessageWithKey(suite.testState, suite.testState.client1, org1key, "topic1", &core.DataRefOrValue{
 		Value: fftypes.JSONAnyPtr(`"test private custom identity"`),
 	}, []string{custom1.DID, custom2.DID}, "tag1", core.TransactionTypeBatchPin, true)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 200, resp.StatusCode())
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
 }
 
 func getUnregisteredAccount(suite *IdentityTestSuite, orgName string) string {
-	verifiers := GetVerifiers(suite.T(), suite.testState.client1)
+	verifiers := e2e.GetVerifiers(suite.T(), suite.testState.client1)
 	suite.T().Logf("checking for account with orgName: %s", orgName)
 	for i, account := range suite.testState.unregisteredAccounts {
-		alreadyRegisted := false
+		alreadyRegistered := false
 		accountMap := account.(map[string]interface{})
 		suite.T().Logf("account from stackState has orgName: %s", accountMap["orgName"])
 		if accountOrgName, ok := accountMap["orgName"]; ok {
@@ -168,15 +169,16 @@ func getUnregisteredAccount(suite *IdentityTestSuite, orgName string) string {
 				suite.T().Logf("verifier value: %s", verifier.Value)
 				if strings.Contains(verifier.Value, key) {
 					// Already registered. Look at the next account
-					alreadyRegisted = true
+					alreadyRegistered = true
 					break
 				}
 			}
 		}
-		if !alreadyRegisted {
+		if !alreadyRegistered {
 			suite.testState.unregisteredAccounts = append(suite.testState.unregisteredAccounts[:i], suite.testState.unregisteredAccounts[i+1:]...)
 			return key
 		}
 	}
+	suite.T().Logf("could not find an unregistered account!")
 	return ""
 }
diff --git a/test/e2e/e2e_test.go b/test/e2e/multiparty/multiparty_test.go
similarity index 52%
rename from test/e2e/e2e_test.go
rename to test/e2e/multiparty/multiparty_test.go
index 52de56510..93cd99295 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/multiparty/multiparty_test.go
@@ -14,18 +14,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"context"
 	"crypto/sha256"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
 	"os"
-	"strings"
 	"testing"
 	"time"
 
@@ -33,151 +31,42 @@ import (
 	"github.com/gorilla/websocket"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 type testState struct {
-	startTime            time.Time
 	t                    *testing.T
-	client1              *resty.Client
-	client2              *resty.Client
+	startTime            time.Time
+	done                 func()
 	ws1                  *websocket.Conn
 	ws2                  *websocket.Conn
 	org1                 *core.Identity
 	org1key              *core.Verifier
 	org2                 *core.Identity
 	org2key              *core.Verifier
-	done                 func()
-	stackState           *StackState
+	client1              *resty.Client
+	client2              *resty.Client
 	unregisteredAccounts []interface{}
 	namespace            string
 }
 
-var widgetSchemaJSON = []byte(`{
-	"$id": "https://example.com/widget.schema.json",
-	"$schema": "https://json-schema.org/draft/2020-12/schema",
-	"title": "Widget",
-	"type": "object",
-	"properties": {
-		"id": {
-			"type": "string",
-			"description": "The unique identifier for the widget."
-		},
-		"name": {
-			"type": "string",
-			"description": "The person's last name."
-		}
-	},
-	"additionalProperties": false
-}`)
-
-func pollForUp(t *testing.T, client *resty.Client) {
-	var resp *resty.Response
-	var err error
-	for i := 0; i < 3; i++ {
-		resp, err = GetNamespaces(client)
-		if err == nil && resp.StatusCode() == 200 {
-			break
-		}
-		time.Sleep(5 * time.Second)
-	}
-	require.NoError(t, err)
-	assert.Equal(t, 200, resp.StatusCode())
+func (m *testState) T() *testing.T {
+	return m.t
 }
 
-func validateReceivedMessages(ts *testState, client *resty.Client, topic string, msgType core.MessageType, txtype core.TransactionType, count int) (data core.DataArray) {
-	var group *fftypes.Bytes32
-	var messages []*core.Message
-	events := GetMessageEvents(ts.t, client, ts.startTime, topic, 200)
-	for i, event := range events {
-		if event.Message != nil {
-			message := event.Message
-			ts.t.Logf("Message %d: %+v", i, *message)
-			if message.Header.Type != msgType {
-				continue
-			}
-			if group != nil {
-				assert.Equal(ts.t, group.String(), message.Header.Group.String(), "All messages must be same group")
-			}
-			group = message.Header.Group
-			messages = append(messages, message)
-		}
-	}
-	assert.Equal(ts.t, count, len(messages))
-
-	var returnData []*core.Data
-	for idx := 0; idx < len(messages); idx++ {
-		assert.Equal(ts.t, txtype, (messages)[idx].Header.TxType)
-		assert.Equal(ts.t, core.FFStringArray{topic}, (messages)[idx].Header.Topics)
-		assert.Equal(ts.t, topic, (messages)[idx].Header.Topics[0])
-
-		data := GetDataForMessage(ts.t, client, ts.startTime, (messages)[idx].Header.ID)
-		var msgData *core.Data
-		for i, d := range data {
-			ts.t.Logf("Data %d: %+v", i, *d)
-			if *d.ID == *messages[idx].Data[0].ID {
-				msgData = d
-			}
-		}
-		assert.NotNil(ts.t, msgData, "Found data with ID '%s'", messages[idx].Data[0].ID)
-		if group == nil {
-			assert.Equal(ts.t, 1, len(data))
-		}
-
-		returnData = append(returnData, msgData)
-		assert.Equal(ts.t, ts.namespace, msgData.Namespace)
-		expectedHash, err := msgData.CalcHash(context.Background())
-		assert.NoError(ts.t, err)
-		assert.Equal(ts.t, *expectedHash, *msgData.Hash)
-
-		if msgData.Blob != nil {
-			blob := GetBlob(ts.t, client, msgData, 200)
-			assert.NotNil(ts.t, blob)
-			var hash fftypes.Bytes32 = sha256.Sum256(blob)
-			assert.Equal(ts.t, *msgData.Blob.Hash, hash)
-		}
-
-	}
-
-	// Flip data (returned in most recent order) into delivery order
-	return returnData
+func (m *testState) StartTime() time.Time {
+	return m.startTime
 }
 
-func validateAccountBalances(t *testing.T, client *resty.Client, poolID *fftypes.UUID, tokenIndex string, balances map[string]int64) {
-	for key, balance := range balances {
-		account := GetTokenBalance(t, client, poolID, tokenIndex, key)
-		assert.Equal(t, balance, account.Balance.Int().Int64())
-	}
-}
-
-func pickTopic(i int, options []string) string {
-	return options[i%len(options)]
-}
-
-func readStackFile(t *testing.T) *Stack {
-	stackFile := os.Getenv("STACK_FILE")
-	if stackFile == "" {
-		t.Fatal("STACK_FILE must be set")
-	}
-	stack, err := ReadStack(stackFile)
-	assert.NoError(t, err)
-	return stack
-}
-
-func readStackState(t *testing.T) *StackState {
-	stackFile := os.Getenv("STACK_STATE")
-	if stackFile == "" {
-		t.Fatal("STACK_STATE must be set")
-	}
-	stackState, err := ReadStackState(stackFile)
-	assert.NoError(t, err)
-	return stackState
+func (m *testState) Done() func() {
+	return m.done
 }
 
 func beforeE2ETest(t *testing.T) *testState {
-	stack := readStackFile(t)
-	stackState := readStackState(t)
+	stack := e2e.ReadStack(t)
+	stackState := e2e.ReadStackState(t)
 	namespace := "default"
 
 	var authHeader1 http.Header
@@ -186,9 +75,8 @@ func beforeE2ETest(t *testing.T) *testState {
 	ts := &testState{
 		t:                    t,
 		startTime:            time.Now(),
-		client1:              NewResty(t),
-		client2:              NewResty(t),
-		stackState:           stackState,
+		client1:              e2e.NewResty(t),
+		client2:              e2e.NewResty(t),
 		unregisteredAccounts: stackState.Accounts[2:],
 		namespace:            namespace,
 	}
@@ -240,18 +128,16 @@ func beforeE2ETest(t *testing.T) *testState {
 	if os.Getenv("NAMESPACE") != "" {
 		namespace := os.Getenv("NAMESPACE")
 		ts.namespace = namespace
-		CreateNamespaces(ts.client1, namespace)
-		CreateNamespaces(ts.client2, namespace)
 	}
 
 	t.Logf("Client 1: " + ts.client1.HostURL)
 	t.Logf("Client 2: " + ts.client2.HostURL)
-	pollForUp(t, ts.client1)
-	pollForUp(t, ts.client2)
+	e2e.PollForUp(t, ts.client1)
+	e2e.PollForUp(t, ts.client2)
 
 	for {
-		orgsC1 := GetOrgs(t, ts.client1, 200)
-		orgsC2 := GetOrgs(t, ts.client2, 200)
+		orgsC1 := e2e.GetOrgs(t, ts.client1, 200)
+		orgsC2 := e2e.GetOrgs(t, ts.client2, 200)
 		if len(orgsC1) >= 2 && len(orgsC2) >= 2 {
 			// in case there are more than two orgs in the network we need to ensure
 			// we select the same two that were provided in the first two elements
@@ -270,8 +156,8 @@ func beforeE2ETest(t *testing.T) *testState {
 		t.Logf("Waiting for 2 orgs to appear. Currently have: node1=%d node2=%d", len(orgsC1), len(orgsC2))
 		time.Sleep(3 * time.Second)
 	}
-	ts.org1key = GetIdentityBlockchainKeys(t, ts.client1, ts.org1.ID, 200)[0]
-	ts.org2key = GetIdentityBlockchainKeys(t, ts.client2, ts.org2.ID, 200)[0]
+	ts.org1key = e2e.GetIdentityBlockchainKeys(t, ts.client1, ts.org1.ID, 200)[0]
+	ts.org2key = e2e.GetIdentityBlockchainKeys(t, ts.client2, ts.org2.ID, 200)[0]
 	t.Logf("Org1: ID=%s DID=%s Key=%s", ts.org1.DID, ts.org1.ID, ts.org1key.Value)
 	t.Logf("Org2: ID=%s DID=%s Key=%s", ts.org2.DID, ts.org2.ID, ts.org2key.Value)
 
@@ -312,151 +198,60 @@ func beforeE2ETest(t *testing.T) *testState {
 	return ts
 }
 
-func wsReader(conn *websocket.Conn, dbChanges bool) chan *core.EventDelivery {
-	events := make(chan *core.EventDelivery, 100)
-	go func() {
-		for {
-			_, b, err := conn.ReadMessage()
-			if err != nil {
-				fmt.Printf("Websocket %s closing, error: %s\n", conn.RemoteAddr(), err)
-				return
-			}
-			var wsa core.WSActionBase
-			err = json.Unmarshal(b, &wsa)
-			if err != nil {
-				panic(fmt.Errorf("Invalid JSON received on WebSocket: %s", err))
-			}
-			switch wsa.Type {
-			default:
-				var ed core.EventDelivery
-				err = json.Unmarshal(b, &ed)
-				if err != nil {
-					panic(fmt.Errorf("Invalid JSON received on WebSocket: %s", err))
-				}
-				if err == nil {
-					fmt.Printf("Websocket %s event: %s/%s/%s -> %s (tx=%s)\n", conn.RemoteAddr(), ed.Namespace, ed.Type, ed.ID, ed.Reference, ed.Transaction)
-					events <- &ed
-				}
-			}
-		}
-	}()
-	return events
-}
-
-func waitForEvent(t *testing.T, c chan *core.EventDelivery, eventType core.EventType, ref *fftypes.UUID) {
-	for {
-		ed := <-c
-		if ed.Type == eventType && (ref == nil || *ref == *ed.Reference) {
-			t.Logf("Detected '%s' event for ref '%s'", ed.Type, ed.Reference)
-			return
-		}
-		t.Logf("Ignored event '%s'", ed.ID)
-	}
-}
-
-func waitForMessageConfirmed(t *testing.T, c chan *core.EventDelivery, msgType core.MessageType) *core.EventDelivery {
-	for {
-		ed := <-c
-		if ed.Type == core.EventTypeMessageConfirmed && ed.Message != nil && ed.Message.Header.Type == msgType {
-			t.Logf("Detected '%s' event for message '%s' of type '%s'", ed.Type, ed.Message.Header.ID, msgType)
-			return ed
-		}
-		t.Logf("Ignored event '%s'", ed.ID)
-	}
-}
-
-func waitForIdentityConfirmed(t *testing.T, c chan *core.EventDelivery) *core.EventDelivery {
-	for {
-		ed := <-c
-		if ed.Type == core.EventTypeIdentityConfirmed {
-			t.Logf("Detected '%s' event for identity '%s'", ed.Type, ed.Reference)
-			return ed
-		}
-		t.Logf("Ignored event '%s'", ed.ID)
-	}
-}
-
-func waitForContractEvent(t *testing.T, client *resty.Client, c chan *core.EventDelivery, match map[string]interface{}) map[string]interface{} {
-	for {
-		eventDelivery := <-c
-		if eventDelivery.Type == core.EventTypeBlockchainEventReceived {
-			event, err := GetBlockchainEvent(t, client, eventDelivery.Event.Reference.String())
-			if err != nil {
-				t.Logf("WARN: unable to get event: %v", err.Error())
-				continue
-			}
-			eventJSON, ok := event.(map[string]interface{})
-			if !ok {
-				t.Logf("WARN: unable to parse changeEvent: %v", event)
+func validateReceivedMessages(ts *testState, client *resty.Client, topic string, msgType core.MessageType, txtype core.TransactionType, count int) (data core.DataArray) {
+	var group *fftypes.Bytes32
+	var messages []*core.Message
+	events := e2e.GetMessageEvents(ts.t, client, ts.startTime, topic, 200)
+	for i, event := range events {
+		if event.Message != nil {
+			message := event.Message
+			ts.t.Logf("Message %d: %+v", i, *message)
+			if message.Header.Type != msgType {
 				continue
 			}
-			if checkObject(t, match, eventJSON) {
-				return eventJSON
+			if group != nil {
+				assert.Equal(ts.t, group.String(), message.Header.Group.String(), "All messages must be same group")
 			}
+			group = message.Header.Group
+			messages = append(messages, message)
 		}
 	}
-}
-
-func checkObject(t *testing.T, expected interface{}, actual interface{}) bool {
-	match := true
-
-	// check if this is a nested object
-	expectedObject, expectedIsObject := expected.(map[string]interface{})
-	actualObject, actualIsObject := actual.(map[string]interface{})
+	assert.Equal(ts.t, count, len(messages))
 
-	t.Logf("Matching blockchain event: %s", fftypes.JSONObject(actualObject).String())
+	var returnData []*core.Data
+	for idx := 0; idx < len(messages); idx++ {
+		assert.Equal(ts.t, txtype, (messages)[idx].Header.TxType)
+		assert.Equal(ts.t, core.FFStringArray{topic}, (messages)[idx].Header.Topics)
+		assert.Equal(ts.t, topic, (messages)[idx].Header.Topics[0])
 
-	// check if this is an array
-	expectedArray, expectedIsArray := expected.([]interface{})
-	actualArray, actualIsArray := actual.([]interface{})
-	switch {
-	case expectedIsObject && actualIsObject:
-		for expectedKey, expectedValue := range expectedObject {
-			if !checkObject(t, expectedValue, actualObject[expectedKey]) {
-				return false
-			}
-		}
-	case expectedIsArray && actualIsArray:
-		for _, expectedItem := range expectedArray {
-			for j, actualItem := range actualArray {
-				if checkObject(t, expectedItem, actualItem) {
-					break
-				}
-				if j == len(actualArray)-1 {
-					return false
-				}
+		data := e2e.GetDataForMessage(ts.t, client, ts.startTime, (messages)[idx].Header.ID)
+		var msgData *core.Data
+		for i, d := range data {
+			ts.t.Logf("Data %d: %+v", i, *d)
+			if *d.ID == *messages[idx].Data[0].ID {
+				msgData = d
 			}
 		}
-	default:
-		expectedString, expectedIsString := expected.(string)
-		actualString, actualIsString := actual.(string)
-		if expectedIsString && actualIsString {
-			return strings.ToLower(expectedString) == strings.ToLower(actualString)
+		assert.NotNil(ts.t, msgData, "Found data with ID '%s'", messages[idx].Data[0].ID)
+		if group == nil {
+			assert.Equal(ts.t, 1, len(data))
 		}
-		return expected == actual
-	}
-	return match
-}
 
-func verifyAllOperationsSucceeded(t *testing.T, clients []*resty.Client, startTime time.Time) {
-	tries := 3
-	delay := 2 * time.Second
-
-	var pending string
-	for i := 0; i < tries; i++ {
-		pending = ""
-		for _, client := range clients {
-			for _, op := range GetOperations(t, client, startTime) {
-				if op.Status != core.OpStatusSucceeded {
-					pending += fmt.Sprintf("Operation '%s' (%s) on '%s' is not successful\n", op.ID, op.Type, client.BaseURL)
-				}
-			}
-		}
-		if pending == "" {
-			return
+		returnData = append(returnData, msgData)
+		assert.Equal(ts.t, ts.namespace, msgData.Namespace)
+		expectedHash, err := msgData.CalcHash(context.Background())
+		assert.NoError(ts.t, err)
+		assert.Equal(ts.t, *expectedHash, *msgData.Hash)
+
+		if msgData.Blob != nil {
+			blob := e2e.GetBlob(ts.t, client, msgData, 200)
+			assert.NotNil(ts.t, blob)
+			var hash fftypes.Bytes32 = sha256.Sum256(blob)
+			assert.Equal(ts.t, *msgData.Blob.Hash, hash)
 		}
-		time.Sleep(delay)
+
 	}
 
-	assert.Fail(t, pending)
+	// Flip data (returned in most recent order) into delivery order
+	return returnData
 }
diff --git a/test/e2e/onchain_offchain_test.go b/test/e2e/multiparty/onchain_offchain_test.go
similarity index 73%
rename from test/e2e/onchain_offchain_test.go
rename to test/e2e/multiparty/onchain_offchain_test.go
index 2bde1cebd..5fb4a7df1 100644
--- a/test/e2e/onchain_offchain_test.go
+++ b/test/e2e/multiparty/onchain_offchain_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"bytes"
@@ -30,6 +30,7 @@ import (
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
@@ -45,14 +46,14 @@ func (suite *OnChainOffChainTestSuite) BeforeTest(suiteName, testName string) {
 }
 
 func (suite *OnChainOffChainTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
 }
 
 func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, true)
-	received2 := wsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received2 := e2e.WsReader(suite.testState.ws2, true)
 
 	// Broadcast some messages, that should get batched, across two topics
 	totalMessages := 10
@@ -63,19 +64,19 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 		data := &core.DataRefOrValue{
 			Value: value,
 		}
-		topic := pickTopic(i, topics)
+		topic := e2e.PickTopic(i, topics)
 
 		expectedData[topic] = append(expectedData[topic], data)
 
-		resp, err := BroadcastMessage(suite.T(), suite.testState.client1, topic, data, false)
+		resp, err := e2e.BroadcastMessage(suite.T(), suite.testState.client1, topic, data, false)
 		require.NoError(suite.T(), err)
 		assert.Equal(suite.T(), 202, resp.StatusCode())
 	}
 
 	for i := 0; i < totalMessages; i++ {
 		// Wait for all the message-confirmed events, from both participants
-		waitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
-		waitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+		e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+		e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
 	}
 
 	for topic, dataArray := range expectedData {
@@ -93,8 +94,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, true)
-	received2 := wsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received2 := e2e.WsReader(suite.testState.ws2, true)
 
 	var resp *resty.Response
 	value := fftypes.JSONAnyPtr(`"Hello"`)
@@ -109,7 +110,7 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 	}
 
 	// Should be rejected as datatype not known
-	resp, err := BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
+	resp, err := e2e.BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 400, resp.StatusCode())
 	assert.Contains(suite.T(), resp.String(), "FF10195") // datatype not found
@@ -117,11 +118,11 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 	dt := &core.Datatype{
 		Name:    "widget",
 		Version: version,
-		Value:   fftypes.JSONAnyPtrBytes(widgetSchemaJSON),
+		Value:   fftypes.JSONAnyPtrBytes(e2e.WidgetSchemaJSON),
 	}
-	dt = CreateDatatype(suite.T(), suite.testState.client1, dt, true)
+	dt = e2e.CreateDatatype(suite.T(), suite.testState.client1, dt, true)
 
-	resp, err = BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
+	resp, err = e2e.BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 400, resp.StatusCode())
 	assert.Contains(suite.T(), resp.String(), "FF10198") // does not conform
@@ -131,19 +132,19 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 		"name": "mywidget"
 	}`)
 
-	resp, err = BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
+	resp, err = e2e.BroadcastMessage(suite.T(), suite.testState.client1, "topic1", &data, true)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 200, resp.StatusCode())
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
 }
 
 func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, true)
-	received2 := wsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received2 := e2e.WsReader(suite.testState.ws2, true)
 
 	var resp *resty.Response
 	value := fftypes.JSONAnyPtr(`{"foo":"bar"}`)
@@ -158,7 +159,7 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 	}
 
 	// Should be rejected as datatype not known
-	resp, err := PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
+	resp, err := e2e.PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	}, "", core.TransactionTypeBatchPin, true)
@@ -169,11 +170,11 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 	dt := &core.Datatype{
 		Name:    "widget",
 		Version: version,
-		Value:   fftypes.JSONAnyPtrBytes(widgetSchemaJSON),
+		Value:   fftypes.JSONAnyPtrBytes(e2e.WidgetSchemaJSON),
 	}
-	dt = CreateDatatype(suite.T(), suite.testState.client1, dt, true)
+	dt = e2e.CreateDatatype(suite.T(), suite.testState.client1, dt, true)
 
-	resp, err = PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
+	resp, err = e2e.PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	}, "", core.TransactionTypeBatchPin, false)
@@ -186,23 +187,23 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 		"name": "mywidget"
 	}`)
 
-	resp, err = PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
+	resp, err = e2e.PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	}, "", core.TransactionTypeBatchPin, true)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 200, resp.StatusCode())
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
 }
 
 func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	// Send 10 messages, that should get batched, across two topics
 	totalMessages := 10
@@ -213,11 +214,11 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 		data := &core.DataRefOrValue{
 			Value: value,
 		}
-		topic := pickTopic(i, topics)
+		topic := e2e.PickTopic(i, topics)
 
 		expectedData[topic] = append(expectedData[topic], data)
 
-		resp, err := PrivateMessage(suite.testState, suite.testState.client1, topic, data, []string{
+		resp, err := e2e.PrivateMessage(suite.testState, suite.testState.client1, topic, data, []string{
 			suite.testState.org1.Name,
 			suite.testState.org2.Name,
 		}, "", core.TransactionTypeBatchPin, false)
@@ -227,8 +228,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 
 	for i := 0; i < totalMessages; i++ {
 		// Wait for all thel message-confirmed events, from both participants
-		waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
-		waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
+		e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
+		e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
 	}
 
 	for topic, dataArray := range expectedData {
@@ -246,22 +247,22 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 func (suite *OnChainOffChainTestSuite) TestE2EBroadcastBlob() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	var resp *resty.Response
 
-	data, resp, err := BroadcastBlobMessage(suite.T(), suite.testState.client1, "topic1")
+	data, resp, err := e2e.BroadcastBlobMessage(suite.T(), suite.testState.client1, "topic1")
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 202, resp.StatusCode())
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
 	val1 := validateReceivedMessages(suite.testState, suite.testState.client1, "topic1", core.MessageTypeBroadcast, core.TransactionTypeBatchPin, 1)
 	assert.Regexp(suite.T(), "myfile.txt", val1[0].Value.String())
 	assert.Equal(suite.T(), "myfile.txt", val1[0].Blob.Name)
 	assert.Equal(suite.T(), data.Blob.Size, val1[0].Blob.Size)
 
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
 	val2 := validateReceivedMessages(suite.testState, suite.testState.client2, "topic1", core.MessageTypeBroadcast, core.TransactionTypeBatchPin, 1)
 	assert.Regexp(suite.T(), "myfile.txt", val2[0].Value.String())
 	assert.Equal(suite.T(), "myfile.txt", val2[0].Blob.Name)
@@ -272,12 +273,12 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcastBlob() {
 func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	var resp *resty.Response
 
-	data, resp, err := PrivateBlobMessageDatatypeTagged(suite.testState, suite.testState.client1, "topic1", []string{
+	data, resp, err := e2e.PrivateBlobMessageDatatypeTagged(suite.testState, suite.testState.client1, "topic1", []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	})
@@ -286,13 +287,13 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() {
 	assert.Empty(suite.T(), data.Blob.Name)
 	assert.NotNil(suite.T(), data.Blob.Hash)
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate)
 	res1 := validateReceivedMessages(suite.testState, suite.testState.client1, "topic1", core.MessageTypePrivate, core.TransactionTypeBatchPin, 1)
 	assert.Equal(suite.T(), data.Blob.Hash.String(), res1[0].Blob.Hash.String())
 	assert.Empty(suite.T(), res1[0].Blob.Name)
 	assert.Equal(suite.T(), data.Blob.Size, res1[0].Blob.Size)
 
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate)
 	res2 := validateReceivedMessages(suite.testState, suite.testState.client2, "topic1", core.MessageTypePrivate, core.TransactionTypeBatchPin, 1)
 	assert.Equal(suite.T(), data.Blob.Hash.String(), res2[0].Blob.Hash.String())
 	assert.Empty(suite.T(), res2[0].Blob.Name)
@@ -302,8 +303,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() {
 func (suite *OnChainOffChainTestSuite) TestE2EWebhookExchange() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
 	subJSON := fmt.Sprintf(`{
 		"transport": "webhooks",
@@ -320,8 +321,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EWebhookExchange() {
 			"tag": "myrequest"
 		}
 	}`, suite.testState.namespace)
-	CleanupExistingSubscription(suite.T(), suite.testState.client2, suite.testState.namespace, "myhook")
-	sub := CreateSubscription(suite.T(), suite.testState.client2, subJSON, 201)
+	e2e.CleanupExistingSubscription(suite.T(), suite.testState.client2, suite.testState.namespace, "myhook")
+	sub := e2e.CreateSubscription(suite.T(), suite.testState.client2, subJSON, 201)
 	assert.NotNil(suite.T(), sub.ID)
 
 	data := core.DataRefOrValue{
@@ -329,17 +330,17 @@ func (suite *OnChainOffChainTestSuite) TestE2EWebhookExchange() {
 	}
 
 	var resp *resty.Response
-	resp, err := PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
+	resp, err := e2e.PrivateMessage(suite.testState, suite.testState.client1, "topic1", &data, []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	}, "myrequest", core.TransactionTypeBatchPin, false)
 	require.NoError(suite.T(), err)
 	assert.Equal(suite.T(), 202, resp.StatusCode())
 
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate) // request 1
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate) // request 2
-	waitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate) // response 1
-	waitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate) // response 2
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate) // request 1
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate) // request 2
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypePrivate) // response 1
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypePrivate) // response 2
 
 	// When we query the confirmed messages for each receiver, we will see the requests and responses.
 	// We just check the reponses (index 1)
@@ -376,15 +377,15 @@ func (suite *OnChainOffChainTestSuite) TestE2EWebhookRequestReplyNoTx() {
 			"tag": "myrequest"
 		}
 	}`, suite.testState.namespace)
-	CleanupExistingSubscription(suite.T(), suite.testState.client2, suite.testState.namespace, "myhook")
-	sub := CreateSubscription(suite.T(), suite.testState.client2, subJSON, 201)
+	e2e.CleanupExistingSubscription(suite.T(), suite.testState.client2, suite.testState.namespace, "myhook")
+	sub := e2e.CreateSubscription(suite.T(), suite.testState.client2, subJSON, 201)
 	assert.NotNil(suite.T(), sub.ID)
 
 	data := core.DataRefOrValue{
 		Value: fftypes.JSONAnyPtr(`{}`),
 	}
 
-	reply := RequestReply(suite.testState, suite.testState.client1, &data, []string{
+	reply := e2e.RequestReply(suite.testState, suite.testState.client1, &data, []string{
 		suite.testState.org1.Name,
 		suite.testState.org2.Name,
 	}, "myrequest", core.TransactionTypeUnpinned)
diff --git a/test/e2e/tokens_test.go b/test/e2e/multiparty/tokens_test.go
similarity index 62%
rename from test/e2e/tokens_test.go
rename to test/e2e/multiparty/tokens_test.go
index 49113836c..a4247cf29 100644
--- a/test/e2e/tokens_test.go
+++ b/test/e2e/multiparty/tokens_test.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package e2e
+package multiparty
 
 import (
 	"fmt"
@@ -24,6 +24,7 @@ import (
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 )
@@ -36,7 +37,7 @@ type TokensTestSuite struct {
 
 func (suite *TokensTestSuite) SetupSuite() {
 	suite.testState = beforeE2ETest(suite.T())
-	stack := readStackFile(suite.T())
+	stack := e2e.ReadStack(suite.T())
 	suite.connector = stack.TokenProviders[0]
 }
 
@@ -45,16 +46,16 @@ func (suite *TokensTestSuite) BeforeTest(suiteName, testName string) {
 }
 
 func (suite *TokensTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
 }
 
 func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
-	pools := GetTokenPools(suite.T(), suite.testState.client1, time.Unix(0, 0))
+	pools := e2e.GetTokenPools(suite.T(), suite.testState.client1, time.Unix(0, 0))
 	rand.Seed(time.Now().UnixNano())
 	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
 	suite.T().Logf("Pool name: %s", poolName)
@@ -65,11 +66,11 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 		Config: fftypes.JSONObject{},
 	}
 
-	poolResp := CreateTokenPool(suite.T(), suite.testState.client1, pool, false)
+	poolResp := e2e.CreateTokenPool(suite.T(), suite.testState.client1, pool, false)
 	poolID := poolResp.ID
 
-	waitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
-	pools = GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
+	pools = e2e.GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
@@ -77,8 +78,8 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	assert.Equal(suite.T(), core.TokenTypeFungible, pools[0].Type)
 	assert.NotEmpty(suite.T(), pools[0].Locator)
 
-	waitForEvent(suite.T(), received2, core.EventTypePoolConfirmed, poolID)
-	pools = GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypePoolConfirmed, poolID)
+	pools = e2e.GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
@@ -94,10 +95,10 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 		},
 		Pool: poolName,
 	}
-	approvalOut := TokenApproval(suite.T(), suite.testState.client1, approval, false)
+	approvalOut := e2e.TokenApproval(suite.T(), suite.testState.client1, approval, false)
 
-	waitForEvent(suite.T(), received1, core.EventTypeApprovalConfirmed, approvalOut.LocalID)
-	approvals := GetTokenApprovals(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeApprovalConfirmed, approvalOut.LocalID)
+	approvals := e2e.GetTokenApprovals(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 1, len(approvals))
 	assert.Equal(suite.T(), suite.connector, approvals[0].Connector)
 	assert.Equal(suite.T(), true, approvals[0].Approved)
@@ -106,25 +107,25 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1)},
 		Pool:          poolName,
 	}
-	transferOut := MintTokens(suite.T(), suite.testState.client1, transfer, false)
+	transferOut := e2e.MintTokens(suite.T(), suite.testState.client1, transfer, false)
 
-	waitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	transfers := GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	transfers := e2e.GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 1, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeMint, transfers[0].Type)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 1,
 	})
 
-	waitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, nil)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, nil)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 1, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeMint, transfers[0].Type)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 1,
 	})
 
@@ -144,29 +145,29 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 			},
 		},
 	}
-	transferOut = TransferTokens(suite.T(), suite.testState.client2, transfer, false)
+	transferOut = e2e.TransferTokens(suite.T(), suite.testState.client2, transfer, false)
 
-	waitForEvent(suite.T(), received1, core.EventTypeMessageConfirmed, transferOut.Message)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeMessageConfirmed, transferOut.Message)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 2, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeTransfer, transfers[0].Type)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	data := GetDataForMessage(suite.T(), suite.testState.client1, suite.testState.startTime, transfers[0].Message)
+	data := e2e.GetDataForMessage(suite.T(), suite.testState.client1, suite.testState.startTime, transfers[0].Message)
 	assert.Equal(suite.T(), 1, len(data))
 	assert.Equal(suite.T(), `"token approval - payment for data"`, data[0].Value.String())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 1,
 	})
 
-	waitForEvent(suite.T(), received2, core.EventTypeMessageConfirmed, transferOut.Message)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeMessageConfirmed, transferOut.Message)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 2, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeTransfer, transfers[0].Type)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 1,
 	})
@@ -175,54 +176,54 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 		TokenTransfer: core.TokenTransfer{Amount: *fftypes.NewFFBigInt(1)},
 		Pool:          poolName,
 	}
-	transferOut = BurnTokens(suite.T(), suite.testState.client2, transfer, false)
+	transferOut = e2e.BurnTokens(suite.T(), suite.testState.client2, transfer, false)
 
-	waitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 3, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeBurn, transfers[0].Type)
 	assert.Equal(suite.T(), "", transfers[0].TokenIndex)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 0,
 	})
 
-	waitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, nil)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, nil)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 3, len(transfers))
 	assert.Equal(suite.T(), suite.connector, transfers[0].Connector)
 	assert.Equal(suite.T(), core.TokenTransferTypeBurn, transfers[0].Type)
 	assert.Equal(suite.T(), "", transfers[0].TokenIndex)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 0,
 	})
 
-	accounts := GetTokenAccounts(suite.T(), suite.testState.client1, poolID)
+	accounts := e2e.GetTokenAccounts(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 2, len(accounts))
 	assert.Equal(suite.T(), suite.testState.org2key.Value, accounts[0].Key)
 	assert.Equal(suite.T(), suite.testState.org1key.Value, accounts[1].Key)
-	accounts = GetTokenAccounts(suite.T(), suite.testState.client2, poolID)
+	accounts = e2e.GetTokenAccounts(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 2, len(accounts))
 	assert.Equal(suite.T(), suite.testState.org2key.Value, accounts[0].Key)
 	assert.Equal(suite.T(), suite.testState.org1key.Value, accounts[1].Key)
 
-	accountPools := GetTokenAccountPools(suite.T(), suite.testState.client1, suite.testState.org1key.Value)
+	accountPools := e2e.GetTokenAccountPools(suite.T(), suite.testState.client1, suite.testState.org1key.Value)
 	assert.Equal(suite.T(), *poolID, *accountPools[0].Pool)
-	accountPools = GetTokenAccountPools(suite.T(), suite.testState.client2, suite.testState.org2key.Value)
+	accountPools = e2e.GetTokenAccountPools(suite.T(), suite.testState.client2, suite.testState.org2key.Value)
 	assert.Equal(suite.T(), *poolID, *accountPools[0].Pool)
 }
 
 func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 	defer suite.testState.done()
 
-	received1 := wsReader(suite.testState.ws1, false)
-	received2 := wsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received2 := e2e.WsReader(suite.testState.ws2, false)
 
-	pools := GetTokenPools(suite.T(), suite.testState.client1, time.Unix(0, 0))
+	pools := e2e.GetTokenPools(suite.T(), suite.testState.client1, time.Unix(0, 0))
 	rand.Seed(time.Now().UnixNano())
 	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
 	suite.T().Logf("Pool name: %s", poolName)
@@ -233,7 +234,7 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 		Config: fftypes.JSONObject{},
 	}
 
-	poolOut := CreateTokenPool(suite.T(), suite.testState.client1, pool, true)
+	poolOut := e2e.CreateTokenPool(suite.T(), suite.testState.client1, pool, true)
 	assert.Equal(suite.T(), suite.testState.namespace, poolOut.Namespace)
 	assert.Equal(suite.T(), poolName, poolOut.Name)
 	assert.Equal(suite.T(), core.TokenTypeNonFungible, poolOut.Type)
@@ -241,9 +242,9 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 
 	poolID := poolOut.ID
 
-	waitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
-	waitForEvent(suite.T(), received2, core.EventTypePoolConfirmed, poolID)
-	pools = GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypePoolConfirmed, poolID)
+	pools = e2e.GetTokenPools(suite.T(), suite.testState.client1, suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), poolName, pools[0].Name)
@@ -258,10 +259,10 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 		},
 		Pool: poolName,
 	}
-	approvalOut := TokenApproval(suite.T(), suite.testState.client1, approval, true)
+	approvalOut := e2e.TokenApproval(suite.T(), suite.testState.client1, approval, true)
 
-	waitForEvent(suite.T(), received1, core.EventTypeApprovalConfirmed, approvalOut.LocalID)
-	approvals := GetTokenApprovals(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeApprovalConfirmed, approvalOut.LocalID)
+	approvals := e2e.GetTokenApprovals(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 1, len(approvals))
 	assert.Equal(suite.T(), suite.connector, approvals[0].Connector)
 	assert.Equal(suite.T(), true, approvals[0].Approved)
@@ -273,22 +274,22 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 		},
 		Pool: poolName,
 	}
-	transferOut := MintTokens(suite.T(), suite.testState.client1, transfer, true)
+	transferOut := e2e.MintTokens(suite.T(), suite.testState.client1, transfer, true)
 	assert.Equal(suite.T(), core.TokenTransferTypeMint, transferOut.Type)
 	assert.Equal(suite.T(), "1", transferOut.TokenIndex)
 	assert.Equal(suite.T(), int64(1), transferOut.Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 1,
 	})
 
-	waitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	waitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, nil)
-	transfers := GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, nil)
+	transfers := e2e.GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 1, len(transfers))
 	assert.Equal(suite.T(), core.TokenTransferTypeMint, transfers[0].Type)
 	assert.Equal(suite.T(), "1", transfers[0].TokenIndex)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 1,
 	})
 
@@ -309,26 +310,26 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 			},
 		},
 	}
-	transferOut = TransferTokens(suite.T(), suite.testState.client1, transfer, true)
+	transferOut = e2e.TransferTokens(suite.T(), suite.testState.client1, transfer, true)
 	assert.Equal(suite.T(), core.TokenTransferTypeTransfer, transferOut.Type)
 	assert.Equal(suite.T(), "1", transferOut.TokenIndex)
 	assert.Equal(suite.T(), int64(1), transferOut.Amount.Int().Int64())
-	data := GetDataForMessage(suite.T(), suite.testState.client1, suite.testState.startTime, transferOut.Message)
+	data := e2e.GetDataForMessage(suite.T(), suite.testState.client1, suite.testState.startTime, transferOut.Message)
 	assert.Equal(suite.T(), 1, len(data))
 	assert.Equal(suite.T(), `"ownership change"`, data[0].Value.String())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 1,
 	})
 
-	waitForEvent(suite.T(), received1, core.EventTypeMessageConfirmed, transferOut.Message)
-	waitForEvent(suite.T(), received2, core.EventTypeMessageConfirmed, transferOut.Message)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeMessageConfirmed, transferOut.Message)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeMessageConfirmed, transferOut.Message)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 2, len(transfers))
 	assert.Equal(suite.T(), core.TokenTransferTypeTransfer, transfers[0].Type)
 	assert.Equal(suite.T(), "1", transfers[0].TokenIndex)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 1,
 	})
@@ -340,38 +341,38 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 		},
 		Pool: poolName,
 	}
-	transferOut = BurnTokens(suite.T(), suite.testState.client2, transfer, true)
+	transferOut = e2e.BurnTokens(suite.T(), suite.testState.client2, transfer, true)
 	assert.Equal(suite.T(), core.TokenTransferTypeBurn, transferOut.Type)
 	assert.Equal(suite.T(), "1", transferOut.TokenIndex)
 	assert.Equal(suite.T(), int64(1), transferOut.Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client2, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 0,
 	})
 
-	waitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, transferOut.LocalID)
-	waitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, nil)
-	transfers = GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
+	e2e.WaitForEvent(suite.T(), received2, core.EventTypeTransferConfirmed, transferOut.LocalID)
+	e2e.WaitForEvent(suite.T(), received1, core.EventTypeTransferConfirmed, nil)
+	transfers = e2e.GetTokenTransfers(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 3, len(transfers))
 	assert.Equal(suite.T(), core.TokenTransferTypeBurn, transfers[0].Type)
 	assert.Equal(suite.T(), "1", transfers[0].TokenIndex)
 	assert.Equal(suite.T(), int64(1), transfers[0].Amount.Int().Int64())
-	validateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
+	e2e.ValidateAccountBalances(suite.T(), suite.testState.client1, poolID, "1", map[string]int64{
 		suite.testState.org1key.Value: 0,
 		suite.testState.org2key.Value: 0,
 	})
 
-	accounts := GetTokenAccounts(suite.T(), suite.testState.client1, poolID)
+	accounts := e2e.GetTokenAccounts(suite.T(), suite.testState.client1, poolID)
 	assert.Equal(suite.T(), 2, len(accounts))
 	assert.Equal(suite.T(), suite.testState.org2key.Value, accounts[0].Key)
 	assert.Equal(suite.T(), suite.testState.org1key.Value, accounts[1].Key)
-	accounts = GetTokenAccounts(suite.T(), suite.testState.client2, poolID)
+	accounts = e2e.GetTokenAccounts(suite.T(), suite.testState.client2, poolID)
 	assert.Equal(suite.T(), 2, len(accounts))
 	assert.Equal(suite.T(), suite.testState.org2key.Value, accounts[0].Key)
 	assert.Equal(suite.T(), suite.testState.org1key.Value, accounts[1].Key)
 
-	accountPools := GetTokenAccountPools(suite.T(), suite.testState.client1, suite.testState.org1key.Value)
+	accountPools := e2e.GetTokenAccountPools(suite.T(), suite.testState.client1, suite.testState.org1key.Value)
 	assert.Equal(suite.T(), *poolID, *accountPools[0].Pool)
-	accountPools = GetTokenAccountPools(suite.T(), suite.testState.client2, suite.testState.org2key.Value)
+	accountPools = e2e.GetTokenAccountPools(suite.T(), suite.testState.client2, suite.testState.org2key.Value)
 	assert.Equal(suite.T(), *poolID, *accountPools[0].Pool)
 }
diff --git a/test/e2e/restclient_test.go b/test/e2e/restclient.go
similarity index 93%
rename from test/e2e/restclient_test.go
rename to test/e2e/restclient.go
index 0decd47d7..a9793236a 100644
--- a/test/e2e/restclient_test.go
+++ b/test/e2e/restclient.go
@@ -99,16 +99,6 @@ func GetNamespaces(client *resty.Client) (*resty.Response, error) {
 		Get(urlGetNamespaces)
 }
 
-func CreateNamespaces(client *resty.Client, namespace string) (*resty.Response, error) {
-	namespaceJSON := map[string]interface{}{
-		"description": "Firefly test namespace",
-		"name":        namespace,
-	}
-	return client.R().
-		SetBody(namespaceJSON).
-		Post(urlGetNamespaces)
-}
-
 func GetMessageEvents(t *testing.T, client *resty.Client, startTime time.Time, topic string, expectedStatus int) (events []*core.EnrichedEvent) {
 	path := urlGetEvents
 	resp, err := client.R().
@@ -355,8 +345,8 @@ func BroadcastBlobMessage(t *testing.T, client *resty.Client, topic string) (*co
 	return data, res, err
 }
 
-func PrivateBlobMessageDatatypeTagged(ts *testState, client *resty.Client, topic string, orgNames []string) (*core.Data, *resty.Response, error) {
-	data := CreateBlob(ts.t, client, &core.DatatypeRef{Name: "myblob"})
+func PrivateBlobMessageDatatypeTagged(ts TestState, client *resty.Client, topic string, orgNames []string) (*core.Data, *resty.Response, error) {
+	data := CreateBlob(ts.T(), client, &core.DatatypeRef{Name: "myblob"})
 	members := make([]core.MemberInput, len(orgNames))
 	for i, oName := range orgNames {
 		// We let FireFly resolve the friendly name of the org to the identity
@@ -376,18 +366,18 @@ func PrivateBlobMessageDatatypeTagged(ts *testState, client *resty.Client, topic
 			},
 			Group: &core.InputGroup{
 				Members: members,
-				Name:    fmt.Sprintf("test_%d", ts.startTime.UnixNano()),
+				Name:    fmt.Sprintf("test_%d", ts.StartTime().UnixNano()),
 			},
 		}).
 		Post(urlPrivateMessage)
 	return data, res, err
 }
 
-func PrivateMessage(ts *testState, client *resty.Client, topic string, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType, confirm bool) (*resty.Response, error) {
+func PrivateMessage(ts TestState, client *resty.Client, topic string, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType, confirm bool) (*resty.Response, error) {
 	return PrivateMessageWithKey(ts, client, "", topic, data, orgNames, tag, txType, confirm)
 }
 
-func PrivateMessageWithKey(ts *testState, client *resty.Client, key, topic string, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType, confirm bool) (*resty.Response, error) {
+func PrivateMessageWithKey(ts TestState, client *resty.Client, key, topic string, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType, confirm bool) (*resty.Response, error) {
 	members := make([]core.MemberInput, len(orgNames))
 	for i, oName := range orgNames {
 		// We let FireFly resolve the friendly name of the org to the identity
@@ -409,7 +399,7 @@ func PrivateMessageWithKey(ts *testState, client *resty.Client, key, topic strin
 		InlineData: core.InlineData{data},
 		Group: &core.InputGroup{
 			Members: members,
-			Name:    fmt.Sprintf("test_%d", ts.startTime.UnixNano()),
+			Name:    fmt.Sprintf("test_%d", ts.StartTime().UnixNano()),
 		},
 	}
 	res, err := client.R().
@@ -417,11 +407,11 @@ func PrivateMessageWithKey(ts *testState, client *resty.Client, key, topic strin
 		SetQueryParam("confirm", strconv.FormatBool(confirm)).
 		SetResult(&msg.Message).
 		Post(urlPrivateMessage)
-	ts.t.Logf("Sent private message %s to %+v", msg.Header.ID, msg.Group.Members)
+	ts.T().Logf("Sent private message %s to %+v", msg.Header.ID, msg.Group.Members)
 	return res, err
 }
 
-func RequestReply(ts *testState, client *resty.Client, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType) *core.MessageInOut {
+func RequestReply(ts TestState, client *resty.Client, data *core.DataRefOrValue, orgNames []string, tag string, txType core.TransactionType) *core.MessageInOut {
 	members := make([]core.MemberInput, len(orgNames))
 	for i, oName := range orgNames {
 		// We let FireFly resolve the friendly name of the org to the identity
@@ -439,7 +429,7 @@ func RequestReply(ts *testState, client *resty.Client, data *core.DataRefOrValue
 		InlineData: core.InlineData{data},
 		Group: &core.InputGroup{
 			Members: members,
-			Name:    fmt.Sprintf("test_%d", ts.startTime.UnixNano()),
+			Name:    fmt.Sprintf("test_%d", ts.StartTime().UnixNano()),
 		},
 	}
 	var replyMsg core.MessageInOut
@@ -447,8 +437,8 @@ func RequestReply(ts *testState, client *resty.Client, data *core.DataRefOrValue
 		SetBody(msg).
 		SetResult(&replyMsg).
 		Post(urlRequestMessage)
-	require.NoError(ts.t, err)
-	require.Equal(ts.t, 200, resp.StatusCode(), "POST %s [%d]: %s", urlUploadData, resp.StatusCode(), resp.String())
+	require.NoError(ts.T(), err)
+	require.Equal(ts.T(), 200, resp.StatusCode(), "POST %s [%d]: %s", urlUploadData, resp.StatusCode(), resp.String())
 	return &replyMsg
 }
 
@@ -732,10 +722,10 @@ func CreateFFI(t *testing.T, client *resty.Client, ffi *fftypes.FFI) (interface{
 	return res, err
 }
 
-func CreateContractAPI(t *testing.T, client *resty.Client, name string, FFIReference *fftypes.FFIReference, location *fftypes.JSONAny) (interface{}, error) {
+func CreateContractAPI(t *testing.T, client *resty.Client, name string, ffiReference *fftypes.FFIReference, location *fftypes.JSONAny) (interface{}, error) {
 	apiReqBody := &core.ContractAPI{
 		Name:      name,
-		Interface: FFIReference,
+		Interface: ffiReference,
 		Location:  location,
 	}
 
@@ -751,12 +741,12 @@ func CreateContractAPI(t *testing.T, client *resty.Client, name string, FFIRefer
 	return res, err
 }
 
-func InvokeContractAPIMethod(t *testing.T, client *resty.Client, APIName string, methodName string, input *fftypes.JSONAny) (interface{}, error) {
+func InvokeContractAPIMethod(t *testing.T, client *resty.Client, apiName string, methodName string, input *fftypes.JSONAny) (interface{}, error) {
 	apiReqBody := map[string]interface{}{
 		"input": input,
 	}
 	var res interface{}
-	path := fmt.Sprintf("%s/%s/invoke/%s", urlContractAPI, APIName, methodName)
+	path := fmt.Sprintf("%s/%s/invoke/%s", urlContractAPI, apiName, methodName)
 	resp, err := client.R().
 		SetBody(apiReqBody).
 		SetResult(&res).
@@ -767,12 +757,12 @@ func InvokeContractAPIMethod(t *testing.T, client *resty.Client, APIName string,
 	return res, err
 }
 
-func QueryContractAPIMethod(t *testing.T, client *resty.Client, APIName string, methodName string, input *fftypes.JSONAny) (interface{}, error) {
+func QueryContractAPIMethod(t *testing.T, client *resty.Client, apiName string, methodName string, input *fftypes.JSONAny) (interface{}, error) {
 	apiReqBody := map[string]interface{}{
 		"input": input,
 	}
 	var res interface{}
-	path := fmt.Sprintf("%s/%s/query/%s", urlContractAPI, APIName, methodName)
+	path := fmt.Sprintf("%s/%s/query/%s", urlContractAPI, apiName, methodName)
 	resp, err := client.R().
 		SetBody(apiReqBody).
 		SetResult(&res).
@@ -782,12 +772,12 @@ func QueryContractAPIMethod(t *testing.T, client *resty.Client, APIName string,
 	return res, err
 }
 
-func CreateContractAPIListener(t *testing.T, client *resty.Client, APIName, eventName, topic string) (*core.ContractListener, error) {
+func CreateContractAPIListener(t *testing.T, client *resty.Client, apiName, eventName, topic string) (*core.ContractListener, error) {
 	apiReqBody := map[string]interface{}{
 		"topic": topic,
 	}
 	var listener core.ContractListener
-	path := fmt.Sprintf("%s/%s/listeners/%s", urlContractAPI, APIName, eventName)
+	path := fmt.Sprintf("%s/%s/listeners/%s", urlContractAPI, apiName, eventName)
 	resp, err := client.R().
 		SetBody(apiReqBody).
 		SetResult(&listener).
diff --git a/test/e2e/run.sh b/test/e2e/run.sh
index 8368e7ccc..6d1ddcb5b 100755
--- a/test/e2e/run.sh
+++ b/test/e2e/run.sh
@@ -10,13 +10,13 @@ CLI_VERSION=$(cat $CWD/../../manifest.json | jq -r .cli.tag)
 STACKS_DIR=~/.firefly/stacks
 
 create_accounts() {
-  if [ "$TEST_SUITE" == "TestEthereumE2ESuite" ]; then
+  if [ "$TEST_SUITE" == "TestEthereumMultipartyE2ESuite" ]; then
       # Create 4 new accounts for use in testing
       for i in {1..4}
       do
           $CLI accounts create $STACK_NAME
       done
-  elif [ "$TEST_SUITE" == "TestFabricE2ESuite" ]; then
+  elif [ "$TEST_SUITE" == "TestFabricMultipartyE2ESuite" ]; then
       # Create 4 new accounts for the first org for use in testing
       for i in {1..3}
       do
@@ -61,6 +61,10 @@ if [ -z "${DATABASE_TYPE}" ]; then
   DATABASE_TYPE=sqlite3
 fi
 
+if [ -z "${MULTIPARTY_ENABLED}" ]; then
+  MULTIPARTY_ENABLED=true
+fi
+
 if [ -z "${STACK_FILE}" ]; then
   STACK_FILE=$STACKS_DIR/$STACK_NAME/stack.json
 fi
@@ -78,7 +82,19 @@ if [ -z "${TOKENS_PROVIDER}" ]; then
 fi
 
 if [ -z "${TEST_SUITE}" ]; then
-  TEST_SUITE=TestEthereumE2ESuite
+  if [ "${BLOCKCHAIN_PROVIDER}" == "fabric" ]; then
+    if [ "${MULTIPARTY_ENABLED}" == "true" ]; then
+      TEST_SUITE=TestFabricMultipartyE2ESuite
+    else
+      TEST_SUITE=TestFabricGatewayE2ESuite
+    fi
+  else
+    if [ "${MULTIPARTY_ENABLED}" == "true" ]; then
+      TEST_SUITE=TestEthereumMultipartyE2ESuite
+    else
+      TEST_SUITE=TestEthereumGatewayE2ESuite
+    fi
+  fi
 fi
 
 cd $CWD
@@ -98,7 +114,7 @@ if [ "$DOWNLOAD_CLI" == "true" ]; then
 fi
 
 if [ "$CREATE_STACK" == "true" ]; then
-  $CLI init --prometheus-enabled --database $DATABASE_TYPE $STACK_NAME 2 --blockchain-provider $BLOCKCHAIN_PROVIDER --token-providers $TOKENS_PROVIDER --manifest ../../manifest.json $EXTRA_INIT_ARGS --sandbox-enabled=false
+  $CLI init --prometheus-enabled --database $DATABASE_TYPE $STACK_NAME 2 --blockchain-provider $BLOCKCHAIN_PROVIDER --token-providers $TOKENS_PROVIDER --manifest ../../manifest.json $EXTRA_INIT_ARGS --sandbox-enabled=false --multiparty=$MULTIPARTY_ENABLED
   checkOk $?
 
   $CLI pull $STACK_NAME -r 3
@@ -108,7 +124,7 @@ if [ "$CREATE_STACK" == "true" ]; then
   checkOk $?
 fi
 
-if [ "$TEST_SUITE" == "TestEthereumE2ESuite" ]; then
+if [ "$TEST_SUITE" == "TestEthereumMultipartyE2ESuite" ] || [ "$TEST_SUITE" == "TestEthereumGatewayE2ESuite" ]; then
     export CONTRACT_ADDRESS=$($CLI deploy ethereum $STACK_NAME ../data/simplestorage/simple_storage.json | jq -r '.address')
 fi
 
@@ -120,7 +136,7 @@ checkOk $?
 export STACK_FILE
 export STACK_STATE
 
-go clean -testcache && go test -v . -run $TEST_SUITE
+go clean -testcache && go test -v ./multiparty ./gateway -run $TEST_SUITE
 checkOk $?
 
 if [ "$RESTART" == "true" ]; then
@@ -132,6 +148,6 @@ if [ "$RESTART" == "true" ]; then
 
   create_accounts
 
-  go clean -testcache && go test -v . -run $TEST_SUITE
+  go clean -testcache && go test -v ./multiparty ./gateway -run $TEST_SUITE
   checkOk $?
 fi
\ No newline at end of file
diff --git a/test/e2e/stack.go b/test/e2e/stack.go
index 42f5212af..75df4e9dc 100644
--- a/test/e2e/stack.go
+++ b/test/e2e/stack.go
@@ -74,7 +74,7 @@ func GetMemberHostname(filename string, n int) (string, error) {
 	return stack.Members[n].FireflyHostname, nil
 }
 
-func ReadStack(filename string) (*Stack, error) {
+func ReadStackFile(filename string) (*Stack, error) {
 	jsonBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
 		return nil, err
@@ -97,7 +97,7 @@ func ReadStack(filename string) (*Stack, error) {
 	return stack, nil
 }
 
-func ReadStackState(filename string) (*StackState, error) {
+func ReadStackStateFile(filename string) (*StackState, error) {
 	jsonBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
 		return nil, err