Repository: hyperledger/firefly
Pull request #865 (2022-06-15 21:00:27)

Create firefly contract subscription per namespace
Each namespace now has a unique firefly contract subscription

To support a subscription per namespace, the `fireflyContract` config section has been replaced with the `contracts` array section under the `multiparty` config.

Example:
```
blockchain:
- name: blockchain0
  type: ethereum
  ethereum:
    ethconnect:
      url: http://ethconnect_0:8080
      topic: "0"
      
namespaces:
  default: default
  predefined:
  - name: default
    remoteName: default
    description: Default predefined namespace
    plugins: [database0, blockchain0, dataexchange0, sharedstorage0, erc20_erc721]
    multiparty:
      enabled: true
      org:
        name: org0
        description: org0
        key: 0x123456
      contract:
        - location:
            address: 0x4ae50189462b0e5d52285f59929d037f790771a6 
          firstEvent: oldest
```

 * location is a yaml object containing blockchain plugin configuration
 * `firstEvent` is replacing `fromBlock`, supporting values of newest or oldest

This PR also introduces a new component, multiparty manager, that is responsible for managing the active firefly contract and interfacing with the blockchain plugin on behalf of that contract.

The subscription names will now also contain the name of the namespace it's associated with:
 * Ethereum: `namespaceName_abiName_instanceUniqueHash`
 * Fabric: `namespaceName_event`

codecov-commenter: # [Codecov](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) Report
> Merging [#865](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (ae731eb) into [main](https://codecov.io/gh/hyperledger/firefly/commit/006aa17c9e3892511c0a13bc790e37187ca900e4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (006aa17) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main     #865   +/-   ##
=======================================
  Coverage   99.96%   99.96%           
=======================================
  Files         300      301    +1     
  Lines       19687    19719   +32     
=======================================
+ Hits        19681    19713   +32     
  Misses          5        5           
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) | Coverage Δ | |
|---|---|---|
| [internal/blockchain/ethereum/config.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9ldGhlcmV1bS9jb25maWcuZ28=) | `100.00% <ø> (ø)` | |
| [internal/coreconfig/coreconfig.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvY29yZWNvbmZpZy9jb3JlY29uZmlnLmdv) | `100.00% <ø> (ø)` | |
| [internal/events/event\_manager.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZXZlbnRzL2V2ZW50X21hbmFnZXIuZ28=) | `100.00% <ø> (ø)` | |
| [internal/batchpin/batchpin.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmF0Y2hwaW4vYmF0Y2hwaW4uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/batchpin/operations.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmF0Y2hwaW4vb3BlcmF0aW9ucy5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/ethereum/ethereum.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9ldGhlcmV1bS9ldGhlcmV1bS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/ethereum/eventstream.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9ldGhlcmV1bS9ldmVudHN0cmVhbS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/fabric/fabric.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9mYWJyaWMvZmFicmljLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/events/batch\_pin\_complete.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZXZlbnRzL2JhdGNoX3Bpbl9jb21wbGV0ZS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/events/network\_action.go](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZXZlbnRzL25ldHdvcmtfYWN0aW9uLmdv) | `100.00% <100.00%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/hyperledger/firefly/pull/865/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Last update [006aa17...ae731eb](https://codecov.io/gh/hyperledger/firefly/pull/865?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger).

shorsher: I was not sure if I needed to make changes to the `ContractManager` to use the new `MultipartyManager` instead of directly interfacing with the blockchain plugin. @awrichar I know we spoke about this briefly, but since the contract manager deals with custom contracts I did not think it needs to be aware of the multiparty manager. 

Patch:
diff --git a/Makefile b/Makefile
index f420d2448..89091bdc3 100644
--- a/Makefile
+++ b/Makefile
@@ -75,6 +75,7 @@ $(eval $(call makemock, internal/apiserver,        FFISwaggerGen,      apiserver
 $(eval $(call makemock, internal/apiserver,        Server,             apiservermocks))
 $(eval $(call makemock, internal/metrics,          Manager,            metricsmocks))
 $(eval $(call makemock, internal/operations,       Manager,            operationmocks))
+$(eval $(call makemock, internal/multiparty,       Manager,            multipartymocks))
 
 firefly-nocgo: ${GOFILES}
 		CGO_ENABLED=0 $(VGO) build -o ${BINARY_NAME}-nocgo -ldflags "-X main.buildDate=$(DATE) -X main.buildVersion=$(BUILD_VERSION) -X 'github.com/hyperledger/firefly/cmd.BuildVersionOverride=$(BUILD_VERSION)' -X 'github.com/hyperledger/firefly/cmd.BuildDate=$(DATE)' -X 'github.com/hyperledger/firefly/cmd.BuildCommit=$(GIT_REF)'" -tags=prod -tags=prod -v
diff --git a/docs/reference/config.md b/docs/reference/config.md
index ce8bc3650..c3a11a534 100644
--- a/docs/reference/config.md
+++ b/docs/reference/config.md
@@ -132,10 +132,10 @@ nav_order: 2
 |batchTimeout|How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500`
 |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use fireflyContract[].fromBlock)|Address `string`|`0`
+|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespace.predefined[].multiparty.contract[].location.firstEvent)|Address `string`|`0`
 |headers|Adds custom headers to HTTP requests|`map[string]string`|`<nil>`
 |idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
-|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use fireflyContract[].address)|Address `string`|`<nil>`
+|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespace.predefined[].multiparty.contract[].location.address)|Address `string`|`<nil>`
 |maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
 |prefixLong|The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect|`string`|`firefly`
 |prefixShort|The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect|`string`|`fly`
@@ -211,21 +211,14 @@ nav_order: 2
 |initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
 |maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 
-## blockchain.ethereum.fireflyContract[]
-
-|Key|Description|Type|Default Value|
-|---|-----------|----|-------------|
-|address|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|`<nil>`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream|Address `string`|`<nil>`
-
 ## blockchain.fabric.fabconnect
 
 |Key|Description|Type|Default Value|
 |---|-----------|----|-------------|
 |batchSize|The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream|`int`|`50`
 |batchTimeout|The maximum amount of time to wait for a batch to complete|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500`
-|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)|`string`|`<nil>`
-|channel|The Fabric channel that FireFly will use for BatchPin transactions|`string`|`<nil>`
+|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.chaincode)|`string`|`<nil>`
+|channel|The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.channel)|`string`|`<nil>`
 |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s`
 |headers|Adds custom headers to HTTP requests|`map[string]string`|`<nil>`
@@ -271,13 +264,6 @@ nav_order: 2
 |readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`
 |writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`
 
-## blockchain.fabric.fireflyContract[]
-
-|Key|Description|Type|Default Value|
-|---|-----------|----|-------------|
-|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`<nil>`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream|Address `string`|`<nil>`
-
 ## blockchainevent.cache
 
 |Key|Description|Type|Default Value|
@@ -618,6 +604,13 @@ nav_order: 2
 |---|-----------|----|-------------|
 |enabled|Enables multi-party mode for this namespace (defaults to true if an org name or key is configured, either here or at the root level)|`boolean`|`<nil>`
 
+## namespaces.predefined[].multiparty.contract[]
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|firstEvent|The first event the contract should process. Valid options are `oldest` or `newest`|`string`|`<nil>`
+|location|A blockchain-specific contract location. For example, an Ethereum contract address, or a Fabric chaincode name and channe|`string`|`<nil>`
+
 ## namespaces.predefined[].multiparty.org
 
 |Key|Description|Type|Default Value|
@@ -816,13 +809,6 @@ nav_order: 2
 |initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
 |maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
 
-## plugins.blockchain[].ethereum.fireflyContract[]
-
-|Key|Description|Type|Default Value|
-|---|-----------|----|-------------|
-|address|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|`<nil>`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream|Address `string`|`<nil>`
-
 ## plugins.blockchain[].fabric.fabconnect
 
 |Key|Description|Type|Default Value|
@@ -876,13 +862,6 @@ nav_order: 2
 |readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`
 |writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb`
 
-## plugins.blockchain[].fabric.fireflyContract[]
-
-|Key|Description|Type|Default Value|
-|---|-----------|----|-------------|
-|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`<nil>`
-|fromBlock|The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream|Address `string`|`<nil>`
-
 ## plugins.database[]
 
 |Key|Description|Type|Default Value|
diff --git a/docs/reference/types/namespace.md b/docs/reference/types/namespace.md
index 901a967b8..85b04f38b 100644
--- a/docs/reference/types/namespace.md
+++ b/docs/reference/types/namespace.md
@@ -62,7 +62,9 @@ nav_order: 20
 |------------|-------------|------|
 | `index` | The index of this contract in the config file | `int` |
 | `finalEvent` | The identifier for the final blockchain event received from this contract before termination | `string` |
-| `info` | Blockchain-specific info on the contract, such as its location on chain | [`JSONObject`](simpletypes#jsonobject) |
+| `location` | A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel | [`JSONAny`](simpletypes#jsonany) |
+| `firstEvent` | A blockchain specific string, such as a block number, to start listening from. The special strings 'oldest' and 'newest' are supported by all blockchain connectors | `string` |
+| `subscription` | The backend identifier of the subscription for the FireFly BatchPin contract | `string` |
 
 
 
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index 42cedbc04..ee397f80d 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -8729,17 +8729,24 @@ paths:
                               description: The identifier for the final blockchain
                                 event received from this contract before termination
                               type: string
+                            firstEvent:
+                              description: A blockchain specific string, such as a
+                                block number, to start listening from. The special
+                                strings 'oldest' and 'newest' are supported by all
+                                blockchain connectors
+                              type: string
                             index:
                               description: The index of this contract in the config
                                 file
                               type: integer
-                            info:
-                              additionalProperties:
-                                description: Blockchain-specific info on the contract,
-                                  such as its location on chain
-                              description: Blockchain-specific info on the contract,
-                                such as its location on chain
-                              type: object
+                            location:
+                              description: A blockchain specific contract identifier.
+                                For example an Ethereum contract address, or a Fabric
+                                chaincode name and channel
+                            subscription:
+                              description: The backend identifier of the subscription
+                                for the FireFly BatchPin contract
+                              type: string
                           type: object
                         terminated:
                           description: Previously-terminated FireFly smart contracts
@@ -8750,17 +8757,24 @@ paths:
                                 description: The identifier for the final blockchain
                                   event received from this contract before termination
                                 type: string
+                              firstEvent:
+                                description: A blockchain specific string, such as
+                                  a block number, to start listening from. The special
+                                  strings 'oldest' and 'newest' are supported by all
+                                  blockchain connectors
+                                type: string
                               index:
                                 description: The index of this contract in the config
                                   file
                                 type: integer
-                              info:
-                                additionalProperties:
-                                  description: Blockchain-specific info on the contract,
-                                    such as its location on chain
-                                description: Blockchain-specific info on the contract,
-                                  such as its location on chain
-                                type: object
+                              location:
+                                description: A blockchain specific contract identifier.
+                                  For example an Ethereum contract address, or a Fabric
+                                  chaincode name and channel
+                              subscription:
+                                description: The backend identifier of the subscription
+                                  for the FireFly BatchPin contract
+                                type: string
                             type: object
                           type: array
                       type: object
@@ -8835,17 +8849,24 @@ paths:
                             description: The identifier for the final blockchain event
                               received from this contract before termination
                             type: string
+                          firstEvent:
+                            description: A blockchain specific string, such as a block
+                              number, to start listening from. The special strings
+                              'oldest' and 'newest' are supported by all blockchain
+                              connectors
+                            type: string
                           index:
                             description: The index of this contract in the config
                               file
                             type: integer
-                          info:
-                            additionalProperties:
-                              description: Blockchain-specific info on the contract,
-                                such as its location on chain
-                            description: Blockchain-specific info on the contract,
-                              such as its location on chain
-                            type: object
+                          location:
+                            description: A blockchain specific contract identifier.
+                              For example an Ethereum contract address, or a Fabric
+                              chaincode name and channel
+                          subscription:
+                            description: The backend identifier of the subscription
+                              for the FireFly BatchPin contract
+                            type: string
                         type: object
                       terminated:
                         description: Previously-terminated FireFly smart contracts
@@ -8856,17 +8877,24 @@ paths:
                               description: The identifier for the final blockchain
                                 event received from this contract before termination
                               type: string
+                            firstEvent:
+                              description: A blockchain specific string, such as a
+                                block number, to start listening from. The special
+                                strings 'oldest' and 'newest' are supported by all
+                                blockchain connectors
+                              type: string
                             index:
                               description: The index of this contract in the config
                                 file
                               type: integer
-                            info:
-                              additionalProperties:
-                                description: Blockchain-specific info on the contract,
-                                  such as its location on chain
-                              description: Blockchain-specific info on the contract,
-                                such as its location on chain
-                              type: object
+                            location:
+                              description: A blockchain specific contract identifier.
+                                For example an Ethereum contract address, or a Fabric
+                                chaincode name and channel
+                            subscription:
+                              description: The backend identifier of the subscription
+                                for the FireFly BatchPin contract
+                              type: string
                           type: object
                         type: array
                     type: object
diff --git a/internal/batchpin/batchpin.go b/internal/batchpin/batchpin.go
index 61abda39a..247e23dbf 100644
--- a/internal/batchpin/batchpin.go
+++ b/internal/batchpin/batchpin.go
@@ -24,8 +24,8 @@ import (
 	"github.com/hyperledger/firefly/internal/coremsgs"
 	"github.com/hyperledger/firefly/internal/identity"
 	"github.com/hyperledger/firefly/internal/metrics"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/operations"
-	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/hyperledger/firefly/pkg/database"
 )
@@ -44,20 +44,20 @@ type batchPinSubmitter struct {
 	namespace  string
 	database   database.Plugin
 	identity   identity.Manager
-	blockchain blockchain.Plugin
+	multiparty multiparty.Manager
 	metrics    metrics.Manager
 	operations operations.Manager
 }
 
-func NewBatchPinSubmitter(ctx context.Context, ns string, di database.Plugin, im identity.Manager, bi blockchain.Plugin, mm metrics.Manager, om operations.Manager) (Submitter, error) {
-	if di == nil || im == nil || bi == nil || mm == nil || om == nil {
+func NewBatchPinSubmitter(ctx context.Context, ns string, di database.Plugin, im identity.Manager, multiparty multiparty.Manager, mm metrics.Manager, om operations.Manager) (Submitter, error) {
+	if di == nil || im == nil || mm == nil || om == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "BatchPinSubmitter")
 	}
 	bp := &batchPinSubmitter{
 		namespace:  ns,
 		database:   di,
 		identity:   im,
-		blockchain: bi,
+		multiparty: multiparty,
 		metrics:    mm,
 		operations: om,
 	}
@@ -74,7 +74,7 @@ func (bp *batchPinSubmitter) Name() string {
 func (bp *batchPinSubmitter) SubmitPinnedBatch(ctx context.Context, batch *core.BatchPersisted, contexts []*fftypes.Bytes32, payloadRef string) error {
 	// The pending blockchain transaction
 	op := core.NewOperation(
-		bp.blockchain,
+		bp.multiparty,
 		batch.Namespace,
 		batch.TX.ID,
 		core.OpTypeBlockchainPinBatch)
diff --git a/internal/batchpin/operations.go b/internal/batchpin/operations.go
index b271643f3..6ad8165fa 100644
--- a/internal/batchpin/operations.go
+++ b/internal/batchpin/operations.go
@@ -86,7 +86,7 @@ func (bp *batchPinSubmitter) RunOperation(ctx context.Context, op *core.Prepared
 	switch data := op.Data.(type) {
 	case batchPinData:
 		batch := data.Batch
-		return nil, false, bp.blockchain.SubmitBatchPin(ctx, op.NamespacedIDString(), batch.Key, &blockchain.BatchPin{
+		return nil, false, bp.multiparty.SubmitBatchPin(ctx, op.NamespacedIDString(), batch.Key, &blockchain.BatchPin{
 			Namespace:       batch.Namespace,
 			TransactionID:   batch.TX.ID,
 			BatchID:         batch.ID,
diff --git a/internal/blockchain/ethereum/config.go b/internal/blockchain/ethereum/config.go
index 7bca63bfb..edb11479d 100644
--- a/internal/blockchain/ethereum/config.go
+++ b/internal/blockchain/ethereum/config.go
@@ -93,11 +93,6 @@ func (e *Ethereum) InitConfig(config config.Section) {
 	e.ethconnectConf.AddKnownKey(EthconnectConfigInstanceDeprecated)
 	e.ethconnectConf.AddKnownKey(EthconnectConfigFromBlockDeprecated, defaultFromBlock)
 
-	e.contractConf = config.SubArray(FireFlyContractConfigKey)
-	e.contractConf.AddKnownKey(FireFlyContractAddress)
-	e.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest")
-	e.contractConfSize = e.contractConf.ArraySize()
-
 	fftmConf := config.SubSection(FFTMConfigKey)
 	ffresty.InitConfig(fftmConf)
 
diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index 4474b9dba..9449a8ffc 100644
--- a/internal/blockchain/ethereum/ethereum.go
+++ b/internal/blockchain/ethereum/ethereum.go
@@ -24,7 +24,6 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
-	"sync"
 
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/config"
@@ -64,20 +63,12 @@ type Ethereum struct {
 	fftmClient      *resty.Client
 	streams         *streamManager
 	streamID        string
-	fireflyContract struct {
-		mux            sync.Mutex
-		address        string
-		fromBlock      string
-		networkVersion int
-		subscription   string
-	}
-	wsconn           wsclient.WSClient
-	closed           chan struct{}
-	addressResolver  *addressResolver
-	metrics          metrics.Manager
-	ethconnectConf   config.Section
-	contractConf     config.ArraySection
-	contractConfSize int
+	wsconn          wsclient.WSClient
+	closed          chan struct{}
+	addressResolver *addressResolver
+	metrics         metrics.Manager
+	ethconnectConf  config.Section
+	subs            map[string]string
 }
 
 type callbacks struct {
@@ -239,6 +230,7 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, metrics metr
 		return err
 	}
 	e.streamID = stream.ID
+	e.subs = make(map[string]string)
 	log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.streamID, e.topic)
 
 	e.closed = make(chan struct{})
@@ -255,103 +247,45 @@ func (e *Ethereum) Start() (err error) {
 	return e.wsconn.Connect()
 }
 
-func (e *Ethereum) resolveFireFlyContract(ctx context.Context, contractIndex int) (address, fromBlock string, err error) {
+func (e *Ethereum) Capabilities() *blockchain.Capabilities {
+	return e.capabilities
+}
 
-	if e.contractConfSize > 0 || contractIndex > 0 {
-		// New config (array of objects under "fireflyContract")
-		if contractIndex >= e.contractConfSize {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.ethereum.fireflyContract[%d]", contractIndex))
-		}
-		entry := e.contractConf.ArrayEntry(contractIndex)
-		address = entry.GetString(FireFlyContractAddress)
-		if address == "" {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.ethereum.fireflyContract")
-		}
-		fromBlock = entry.GetString(FireFlyContractFromBlock)
-	} else {
-		// Old config (attributes under "ethconnect")
-		address = e.ethconnectConf.GetString(EthconnectConfigInstanceDeprecated)
-		if address != "" {
-			log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead",
-				EthconnectConfigKey, EthconnectConfigInstanceDeprecated,
-				FireFlyContractConfigKey, FireFlyContractAddress)
-		} else {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect")
-		}
-		fromBlock = e.ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated)
-		if fromBlock != "" {
-			log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead",
-				EthconnectConfigKey, EthconnectConfigFromBlockDeprecated,
-				FireFlyContractConfigKey, FireFlyContractFromBlock)
-		}
+func (e *Ethereum) AddFireflySubscription(ctx context.Context, namespace string, location *fftypes.JSONAny, firstEvent string) (string, error) {
+	ethLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return "", err
 	}
 
-	// Backwards compatibility from when instance path was not a contract address
-	if strings.HasPrefix(strings.ToLower(address), "/contracts/") {
-		address, err = e.getContractAddress(ctx, address)
-		if err != nil {
-			return "", "", err
-		}
-	} else if strings.HasPrefix(address, "/instances/") {
-		address = strings.Replace(address, "/instances/", "", 1)
+	switch firstEvent {
+	case string(core.SubOptsFirstEventOldest):
+		firstEvent = "0"
+	case string(core.SubOptsFirstEventNewest):
+		firstEvent = "latest"
 	}
 
-	address, err = validateEthAddress(ctx, address)
-	return address, fromBlock, err
-}
-
-func (e *Ethereum) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) {
-
-	log.L(ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index)
-	address, fromBlock, err := e.resolveFireFlyContract(ctx, contracts.Active.Index)
+	sub, err := e.streams.ensureFireFlySubscription(ctx, namespace, ethLocation.Address, firstEvent, e.streamID, batchPinEventABI)
 	if err != nil {
-		return err
+		return "", err
 	}
+	// TODO: We will probably need to save the namespace AND network version here
+	// Ultimately there needs to be a logic branch in the event handling, where for "V1" we expect to receive a namespace in every
+	// BatchPin event, but for "V2" we infer the namespace based on which subscription ID produced it.
+	e.subs[sub.ID] = namespace
 
-	sub, err := e.streams.ensureFireFlySubscription(ctx, address, fromBlock, e.streamID, batchPinEventABI)
-	if err == nil {
-		var version int
-		version, err = e.getNetworkVersion(ctx, address)
-		if err == nil {
-			e.fireflyContract.mux.Lock()
-			e.fireflyContract.address = address
-			e.fireflyContract.fromBlock = fromBlock
-			e.fireflyContract.networkVersion = version
-			e.fireflyContract.subscription = sub.ID
-			e.fireflyContract.mux.Unlock()
-			contracts.Active.Info = fftypes.JSONObject{
-				"address":      address,
-				"fromBlock":    fromBlock,
-				"subscription": sub.ID,
-			}
-		}
-	}
-	return err
+	return sub.ID, nil
 }
 
-func (e *Ethereum) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) {
-
-	address, err := validateEthAddress(ctx, termination.Info.GetString("address"))
-	if err != nil {
-		return err
-	}
-	e.fireflyContract.mux.Lock()
-	fireflyAddress := e.fireflyContract.address
-	e.fireflyContract.mux.Unlock()
-	if address != fireflyAddress {
-		log.L(ctx).Warnf("Ignoring termination request from address %s, which differs from active address %s", address, fireflyAddress)
+func (e *Ethereum) RemoveFireflySubscription(ctx context.Context, subID string) error {
+	// Don't actually delete the subscription from ethconnect, as this may be called while processing
+	// events from the subscription (and handling that scenario cleanly could be difficult for ethconnect).
+	// TODO: can old subscriptions be somehow cleaned up later?
+	if _, ok := e.subs[subID]; ok {
+		delete(e.subs, subID)
 		return nil
 	}
 
-	log.L(ctx).Infof("Processing termination request from address %s", address)
-	contracts.Active.FinalEvent = termination.ProtocolID
-	contracts.Terminated = append(contracts.Terminated, contracts.Active)
-	contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1}
-	return e.ConfigureContract(ctx, contracts)
-}
-
-func (e *Ethereum) Capabilities() *blockchain.Capabilities {
-	return e.capabilities
+	return i18n.NewError(ctx, coremsgs.MsgSubscriptionIDInvalid, subID)
 }
 
 func (e *Ethereum) afterConnect(ctx context.Context, w wsclient.WSClient) error {
@@ -541,12 +475,8 @@ func (e *Ethereum) handleMessageBatch(ctx context.Context, messages []interface{
 		l1.Infof("Received '%s' message", signature)
 		l1.Tracef("Message: %+v", msgJSON)
 
-		e.fireflyContract.mux.Lock()
-		fireflySub := e.fireflyContract.subscription
-		e.fireflyContract.mux.Unlock()
-
-		if sub == fireflySub {
-			// Matches the active FireFly BatchPin subscription
+		// Matches one of the active FireFly BatchPin subscriptions
+		if _, ok := e.subs[sub]; ok {
 			switch signature {
 			case broadcastBatchEventSignature:
 				if err := e.handleBatchPinEvent(ctx1, msgJSON); err != nil {
@@ -713,7 +643,7 @@ func (e *Ethereum) queryContractMethod(ctx context.Context, address string, abi
 	return res, nil
 }
 
-func (e *Ethereum) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error {
+func (e *Ethereum) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin, location *fftypes.JSONAny) error {
 	ethHashes := make([]string, len(batch.Contexts))
 	for i, v := range batch.Contexts {
 		ethHashes[i] = ethHexFormatB32(v)
@@ -728,13 +658,16 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey
 		batch.BatchPayloadRef,
 		ethHashes,
 	}
-	e.fireflyContract.mux.Lock()
-	address := e.fireflyContract.address
-	e.fireflyContract.mux.Unlock()
-	return e.invokeContractMethod(ctx, address, signingKey, batchPinMethodABI, nsOpID, input, nil)
+
+	ethLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return err
+	}
+
+	return e.invokeContractMethod(ctx, ethLocation.Address, signingKey, batchPinMethodABI, nsOpID, input, nil)
 }
 
-func (e *Ethereum) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType) error {
+func (e *Ethereum) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType, location *fftypes.JSONAny) error {
 	input := []interface{}{
 		blockchain.FireFlyActionPrefix + action,
 		ethHexFormatB32(nil),
@@ -742,10 +675,11 @@ func (e *Ethereum) SubmitNetworkAction(ctx context.Context, nsOpID string, signi
 		"",
 		[]string{},
 	}
-	e.fireflyContract.mux.Lock()
-	address := e.fireflyContract.address
-	e.fireflyContract.mux.Unlock()
-	return e.invokeContractMethod(ctx, address, signingKey, batchPinMethodABI, nsOpID, input, nil)
+	ethLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return err
+	}
+	return e.invokeContractMethod(ctx, ethLocation.Address, signingKey, batchPinMethodABI, nsOpID, input, nil)
 }
 
 func (e *Ethereum) InvokeContract(ctx context.Context, nsOpID string, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}, options map[string]interface{}) error {
@@ -1177,8 +1111,13 @@ func (e *Ethereum) getFFIType(solidityType string) core.FFIInputType {
 	return ""
 }
 
-func (e *Ethereum) getNetworkVersion(ctx context.Context, address string) (int, error) {
-	res, err := e.queryContractMethod(ctx, address, networkVersionMethodABI, []interface{}{}, nil)
+func (e *Ethereum) GetNetworkVersion(ctx context.Context, location *fftypes.JSONAny) (int, error) {
+	ethLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return 0, err
+	}
+
+	res, err := e.queryContractMethod(ctx, ethLocation.Address, networkVersionMethodABI, []interface{}{}, nil)
 	if err != nil || !res.IsSuccess() {
 		// "Call failed" is interpreted as "method does not exist, default to version 1"
 		if strings.Contains(err.Error(), "FFEC100148") {
@@ -1193,8 +1132,41 @@ func (e *Ethereum) getNetworkVersion(ctx context.Context, address string) (int,
 	return strconv.Atoi(output.Output.(string))
 }
 
-func (e *Ethereum) NetworkVersion() int {
-	e.fireflyContract.mux.Lock()
-	defer e.fireflyContract.mux.Unlock()
-	return e.fireflyContract.networkVersion
+func (e *Ethereum) GetAndConvertDeprecatedContractConfig(ctx context.Context) (location *fftypes.JSONAny, fromBlock string, err error) {
+	// Old config (attributes under "ethconnect")
+	address := e.ethconnectConf.GetString(EthconnectConfigInstanceDeprecated)
+	if address != "" {
+		log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use namespaces.predefined[].multiparty.contract[].location.address instead",
+			EthconnectConfigKey, EthconnectConfigInstanceDeprecated)
+	} else {
+		return nil, "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect")
+	}
+
+	fromBlock = e.ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated)
+	if fromBlock != "" {
+		log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use namespaces.predefined[].multiparty.contract[].location.firstEvent instead",
+			EthconnectConfigKey, EthconnectConfigFromBlockDeprecated)
+	}
+
+	// Backwards compatibility from when instance path was not a contract address
+	if strings.HasPrefix(strings.ToLower(address), "/contracts/") {
+		address, err = e.getContractAddress(ctx, address)
+		if err != nil {
+			return nil, "", err
+		}
+	} else if strings.HasPrefix(address, "/instances/") {
+		address = strings.Replace(address, "/instances/", "", 1)
+	}
+	address, err = validateEthAddress(ctx, address)
+	if err != nil {
+		return nil, "", err
+	}
+
+	ethLocation := &Location{
+		Address: address,
+	}
+	normalized, _ := json.Marshal(ethLocation)
+	location = fftypes.JSONAnyPtrBytes(normalized)
+
+	return location, fromBlock, nil
 }
diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go
index 459184639..1f687a3e0 100644
--- a/internal/blockchain/ethereum/eventstream.go
+++ b/internal/blockchain/ethereum/eventstream.go
@@ -171,7 +171,7 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string) er
 	return nil
 }
 
-func (s *streamManager) ensureFireFlySubscription(ctx context.Context, instancePath, fromBlock, stream string, abi *abi.Entry) (sub *subscription, err error) {
+func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, instancePath, fromBlock, stream string, abi *abi.Entry) (sub *subscription, err error) {
 	// Include a hash of the instance path in the subscription, so if we ever point at a different
 	// contract configuration, we re-subscribe from block 0.
 	// We don't need full strength hashing, so just use the first 16 chars for readability.
@@ -182,15 +182,18 @@ func (s *streamManager) ensureFireFlySubscription(ctx context.Context, instanceP
 		return nil, err
 	}
 
-	subName := fmt.Sprintf("%s_%s", abi.Name, instanceUniqueHash)
+	subName := fmt.Sprintf("%s_%s_%s", namespace, abi.Name, instanceUniqueHash)
 
 	for _, s := range existingSubs {
 		if s.Stream == stream && (s.Name == subName ||
-			/* Check for the plain name we used to use originally, before adding uniqueness qualifier.
-			   If one of these very early environments needed a new subscription, the existing one would need to
+			/* Check for the deprecates names, before adding namespace uniqueness qualifier.
+			   NOTE: If one of these very early environments needed a new subscription, the existing one would need to
 				 be deleted manually. */
-			s.Name == abi.Name) {
+			s.Name == abi.Name || s.Name == fmt.Sprintf("%s_%s", abi.Name, instanceUniqueHash)) {
 			sub = s
+			if s.Name == abi.Name || s.Name == fmt.Sprintf("%s_%s", abi.Name, instanceUniqueHash) {
+				log.L(ctx).Warnf("Subscription %s uses deprecated functionality, please upgrade to utilize multiple namespaces.", s.Name)
+			}
 		}
 	}
 
diff --git a/internal/blockchain/fabric/config.go b/internal/blockchain/fabric/config.go
index 86695309b..9cb24a47d 100644
--- a/internal/blockchain/fabric/config.go
+++ b/internal/blockchain/fabric/config.go
@@ -69,9 +69,4 @@ func (f *Fabric) InitConfig(config config.Section) {
 	f.fabconnectConf.AddKnownKey(FabconnectConfigBatchTimeout, defaultBatchTimeout)
 	f.fabconnectConf.AddKnownKey(FabconnectPrefixShort, defaultPrefixShort)
 	f.fabconnectConf.AddKnownKey(FabconnectPrefixLong, defaultPrefixLong)
-
-	f.contractConf = config.SubArray(FireFlyContractConfigKey)
-	f.contractConf.AddKnownKey(FireFlyContractChaincode)
-	f.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest")
-	f.contractConfSize = f.contractConf.ArraySize()
 }
diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go
index 325a2b931..ccc6ba194 100644
--- a/internal/blockchain/fabric/eventstream.go
+++ b/internal/blockchain/fabric/eventstream.go
@@ -18,6 +18,7 @@ package fabric
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/ffresty"
@@ -150,16 +151,19 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string) er
 	return nil
 }
 
-func (s *streamManager) ensureFireFlySubscription(ctx context.Context, location *Location, fromBlock, stream, event string) (sub *subscription, err error) {
+func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, location *Location, fromBlock, stream, event string) (sub *subscription, err error) {
 	existingSubs, err := s.getSubscriptions(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	subName := event
+	subName := fmt.Sprintf("%s_%s", namespace, event)
 	for _, s := range existingSubs {
-		if s.Stream == stream && s.Name == subName {
+		if s.Stream == stream && (s.Name == subName || s.Name == event) {
 			sub = s
+			if s.Name == event {
+				log.L(ctx).Warnf("Subscription %s uses deprecated functionality, please upgrade to utilize multiple namespaces.", s.Name)
+			}
 		}
 	}
 
diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index 353e40812..a0436face 100644
--- a/internal/blockchain/fabric/fabric.go
+++ b/internal/blockchain/fabric/fabric.go
@@ -24,7 +24,6 @@ import (
 	"fmt"
 	"regexp"
 	"strings"
-	"sync"
 
 	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/config"
@@ -44,31 +43,23 @@ const (
 )
 
 type Fabric struct {
-	ctx             context.Context
-	topic           string
-	defaultChannel  string
-	signer          string
-	prefixShort     string
-	prefixLong      string
-	capabilities    *blockchain.Capabilities
-	callbacks       callbacks
-	client          *resty.Client
-	streams         *streamManager
-	streamID        string
-	fireflyContract struct {
-		mux            sync.Mutex
-		chaincode      string
-		fromBlock      string
-		networkVersion int
-		subscription   string
-	}
-	idCache          map[string]*fabIdentity
-	wsconn           wsclient.WSClient
-	closed           chan struct{}
-	metrics          metrics.Manager
-	fabconnectConf   config.Section
-	contractConf     config.ArraySection
-	contractConfSize int
+	ctx            context.Context
+	topic          string
+	defaultChannel string
+	signer         string
+	prefixShort    string
+	prefixLong     string
+	capabilities   *blockchain.Capabilities
+	callbacks      callbacks
+	client         *resty.Client
+	streams        *streamManager
+	streamID       string
+	idCache        map[string]*fabIdentity
+	wsconn         wsclient.WSClient
+	closed         chan struct{}
+	metrics        metrics.Manager
+	fabconnectConf config.Section
+	subs           map[string]string
 }
 
 type callbacks struct {
@@ -239,6 +230,7 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, metrics metric
 		return err
 	}
 	f.streamID = stream.ID
+	f.subs = make(map[string]string)
 	log.L(f.ctx).Infof("Event stream: %s", f.streamID)
 
 	f.closed = make(chan struct{})
@@ -247,81 +239,6 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, metrics metric
 	return nil
 }
 
-func (f *Fabric) resolveFireFlyContract(ctx context.Context, contractIndex int) (chaincode, fromBlock string, err error) {
-
-	if f.contractConfSize > 0 || contractIndex > 0 {
-		// New config (array of objects under "fireflyContract")
-		if contractIndex >= f.contractConfSize {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.fabric.fireflyContract[%d]", contractIndex))
-		}
-		chaincode = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractChaincode)
-		if chaincode == "" {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fireflyContract")
-		}
-		fromBlock = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock)
-	} else {
-		// Old config (attributes under "ethconnect")
-		chaincode = f.fabconnectConf.GetString(FabconnectConfigChaincodeDeprecated)
-		if chaincode != "" {
-			log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead",
-				FabconnectConfigKey, FabconnectConfigChaincodeDeprecated,
-				FireFlyContractConfigKey, FireFlyContractChaincode)
-			fromBlock = string(core.SubOptsFirstEventOldest)
-		} else {
-			return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fabconnect")
-		}
-	}
-
-	return chaincode, fromBlock, nil
-}
-
-func (f *Fabric) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) {
-
-	chaincode, fromBlock, err := f.resolveFireFlyContract(ctx, contracts.Active.Index)
-	if err != nil {
-		return err
-	}
-
-	location := &Location{Channel: f.defaultChannel, Chaincode: chaincode}
-	sub, err := f.streams.ensureFireFlySubscription(ctx, location, fromBlock, f.streamID, batchPinEvent)
-	if err == nil {
-		var version int
-		version, err = f.getNetworkVersion(ctx, chaincode)
-		if err == nil {
-			f.fireflyContract.mux.Lock()
-			f.fireflyContract.chaincode = chaincode
-			f.fireflyContract.fromBlock = fromBlock
-			f.fireflyContract.networkVersion = version
-			f.fireflyContract.subscription = sub.ID
-			f.fireflyContract.mux.Unlock()
-			contracts.Active.Info = fftypes.JSONObject{
-				"chaincode":    chaincode,
-				"fromBlock":    fromBlock,
-				"subscription": sub.ID,
-			}
-		}
-	}
-	return err
-}
-
-func (f *Fabric) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) {
-
-	chaincode := termination.Info.GetString("chaincodeId")
-	f.fireflyContract.mux.Lock()
-	fireflyChaincode := f.fireflyContract.chaincode
-	f.fireflyContract.mux.Unlock()
-	if chaincode != fireflyChaincode {
-		log.L(ctx).Warnf("Ignoring termination request from chaincode %s, which differs from active chaincode %s", chaincode, fireflyChaincode)
-		return nil
-	}
-
-	log.L(ctx).Infof("Processing termination request from chaincode %s", chaincode)
-	contracts.Active.FinalEvent = termination.ProtocolID
-	contracts.Terminated = append(contracts.Terminated, contracts.Active)
-	contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1}
-	return f.ConfigureContract(ctx, contracts)
-}
-
 func (f *Fabric) SetHandler(handler blockchain.Callbacks) {
 	f.callbacks.handlers = append(f.callbacks.handlers, handler)
 }
@@ -495,6 +412,33 @@ func (f *Fabric) handleReceipt(ctx context.Context, reply fftypes.JSONObject) {
 	f.callbacks.BlockchainOpUpdate(f, requestID, updateType, txHash, message, reply)
 }
 
+func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace string, location *fftypes.JSONAny, firstEvent string) (string, error) {
+	fabricOnChainLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return "", err
+	}
+
+	sub, err := f.streams.ensureFireFlySubscription(ctx, namespace, fabricOnChainLocation, firstEvent, f.streamID, batchPinEvent)
+	if err != nil {
+		return "", err
+	}
+	f.subs[sub.ID] = namespace
+
+	return sub.ID, nil
+}
+
+func (f *Fabric) RemoveFireflySubscription(ctx context.Context, subID string) error {
+	// Don't actually delete the subscription from fabconnect, as this may be called while processing
+	// events from the subscription (and handling that scenario cleanly could be difficult for fabconnect).
+	// TODO: can old subscriptions be somehow cleaned up later?
+	if _, ok := f.subs[subID]; ok {
+		delete(f.subs, subID)
+		return nil
+	}
+
+	return i18n.NewError(ctx, coremsgs.MsgSubscriptionIDInvalid, subID)
+}
+
 func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{}) error {
 	l := log.L(ctx)
 
@@ -513,12 +457,8 @@ func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{})
 		l1.Infof("Received '%s' message", eventName)
 		l1.Tracef("Message: %+v", msgJSON)
 
-		f.fireflyContract.mux.Lock()
-		fireflySub := f.fireflyContract.subscription
-		f.fireflyContract.mux.Unlock()
-
-		if sub == fireflySub {
-			// Matches the active FireFly BatchPin subscription
+		// Matches one of the active FireFly BatchPin subscriptions
+		if _, ok := f.subs[sub]; ok {
 			switch eventName {
 			case broadcastBatchEventName:
 				if err := f.handleBatchPinEvent(ctx1, msgJSON); err != nil {
@@ -675,7 +615,12 @@ func hexFormatB32(b *fftypes.Bytes32) string {
 	return "0x" + hex.EncodeToString(b[0:32])
 }
 
-func (f *Fabric) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error {
+func (f *Fabric) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin, location *fftypes.JSONAny) error {
+	fabricOnChainLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return err
+	}
+
 	hashes := make([]string, len(batch.Contexts))
 	for i, v := range batch.Contexts {
 		hashes[i] = hexFormatB32(v)
@@ -691,13 +636,15 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey s
 		"contexts":   hashes,
 	}
 	input, _ := jsonEncodeInput(pinInput)
-	f.fireflyContract.mux.Lock()
-	chaincode := f.fireflyContract.chaincode
-	f.fireflyContract.mux.Unlock()
-	return f.invokeContractMethod(ctx, f.defaultChannel, chaincode, batchPinMethodName, signingKey, nsOpID, batchPinPrefixItems, input, nil)
+	return f.invokeContractMethod(ctx, fabricOnChainLocation.Channel, fabricOnChainLocation.Chaincode, batchPinMethodName, signingKey, nsOpID, batchPinPrefixItems, input, nil)
 }
 
-func (f *Fabric) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType) error {
+func (f *Fabric) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType, location *fftypes.JSONAny) error {
+	fabricOnChainLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return err
+	}
+
 	pinInput := map[string]interface{}{
 		"namespace":  "firefly:" + action,
 		"uuids":      hexFormatB32(nil),
@@ -706,10 +653,7 @@ func (f *Fabric) SubmitNetworkAction(ctx context.Context, nsOpID string, signing
 		"contexts":   []string{},
 	}
 	input, _ := jsonEncodeInput(pinInput)
-	f.fireflyContract.mux.Lock()
-	chaincode := f.fireflyContract.chaincode
-	f.fireflyContract.mux.Unlock()
-	return f.invokeContractMethod(ctx, f.defaultChannel, chaincode, batchPinMethodName, signingKey, nsOpID, batchPinPrefixItems, input, nil)
+	return f.invokeContractMethod(ctx, fabricOnChainLocation.Channel, fabricOnChainLocation.Chaincode, batchPinMethodName, signingKey, nsOpID, batchPinPrefixItems, input, nil)
 }
 
 func (f *Fabric) buildFabconnectRequestBody(ctx context.Context, channel, chaincode, methodName, signingKey, requestID string, prefixItems []*PrefixItem, input map[string]interface{}, options map[string]interface{}) (map[string]interface{}, error) {
@@ -866,8 +810,13 @@ func (f *Fabric) GenerateEventSignature(ctx context.Context, event *core.FFIEven
 	return event.Name
 }
 
-func (f *Fabric) getNetworkVersion(ctx context.Context, chaincode string) (int, error) {
-	res, err := f.queryContractMethod(ctx, f.defaultChannel, chaincode, networkVersionMethodName, f.signer, "", []*PrefixItem{}, map[string]interface{}{}, nil)
+func (f *Fabric) GetNetworkVersion(ctx context.Context, location *fftypes.JSONAny) (int, error) {
+	fabricOnChainLocation, err := parseContractLocation(ctx, location)
+	if err != nil {
+		return 0, err
+	}
+
+	res, err := f.queryContractMethod(ctx, fabricOnChainLocation.Channel, fabricOnChainLocation.Chaincode, networkVersionMethodName, f.signer, "", []*PrefixItem{}, map[string]interface{}{}, nil)
 	if err != nil || !res.IsSuccess() {
 		// "Function not found" is interpreted as "default to version 1"
 		notFoundError := fmt.Sprintf("Function %s not found", networkVersionMethodName)
@@ -883,8 +832,24 @@ func (f *Fabric) getNetworkVersion(ctx context.Context, chaincode string) (int,
 	return int(output.Result.(float64)), nil
 }
 
-func (f *Fabric) NetworkVersion() int {
-	f.fireflyContract.mux.Lock()
-	defer f.fireflyContract.mux.Unlock()
-	return f.fireflyContract.networkVersion
+func (f *Fabric) GetAndConvertDeprecatedContractConfig(ctx context.Context) (location *fftypes.JSONAny, fromBlock string, err error) {
+	// Old config (attributes under "fabconnect")
+	chaincode := f.fabconnectConf.GetString(FabconnectConfigChaincodeDeprecated)
+	if chaincode != "" {
+		log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use namespaces.predefined[].multiparty.contract[].location.address",
+			FabconnectConfigKey, FabconnectConfigChaincodeDeprecated)
+	} else {
+		return nil, "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fabconnect")
+	}
+	fromBlock = string(core.SubOptsFirstEventOldest)
+
+	fabricOnChainLocation := &Location{
+		Chaincode: chaincode,
+		Channel:   f.defaultChannel,
+	}
+
+	normalized, _ := json.Marshal(fabricOnChainLocation)
+	location = fftypes.JSONAnyPtrBytes(normalized)
+
+	return location, fromBlock, nil
 }
diff --git a/internal/coreconfig/coreconfig.go b/internal/coreconfig/coreconfig.go
index a60b11d47..84d771024 100644
--- a/internal/coreconfig/coreconfig.go
+++ b/internal/coreconfig/coreconfig.go
@@ -39,14 +39,22 @@ const (
 	NamespacePlugins = "plugins"
 	// NamespaceDefaultKey is the default signing key for blockchain transactions within this namespace
 	NamespaceDefaultKey = "defaultKey"
+	// NamespaceMultiparty contains the multiparty configuration for a namespace
+	NamespaceMultiparty = "multiparty"
 	// NamespaceMultipartyEnabled specifies if multi-party mode is enabled for a namespace
-	NamespaceMultipartyEnabled = "multiparty.enabled"
+	NamespaceMultipartyEnabled = "enabled"
 	// NamespaceMultipartyOrgName is a short name for the local root org within a namespace
-	NamespaceMultipartyOrgName = "multiparty.org.name"
+	NamespaceMultipartyOrgName = "org.name"
 	// NamespaceMultipartyOrgDescription is a description for the local root org within a namespace
-	NamespaceMultipartyOrgDescription = "multiparty.org.description"
+	NamespaceMultipartyOrgDescription = "org.description"
 	// NamespaceMultipartyOrgKey is the signing key allocated to the local root org within a namespace
-	NamespaceMultipartyOrgKey = "multiparty.org.key"
+	NamespaceMultipartyOrgKey = "org.key"
+	// NamespaceMultipartyContract is a list of firefly contract configurations for this namespace
+	NamespaceMultipartyContract = "contract"
+	// NamespaceMultipartyContractFirstEvent is the first event to process for this contract
+	NamespaceMultipartyContractFirstEvent = "firstEvent"
+	// NamespaceMultipartyContractLocation is an object containing blockchain specific configuration
+	NamespaceMultipartyContractLocation = "location"
 )
 
 // The following keys can be access from the root configuration.
diff --git a/internal/coremsgs/en_config_descriptions.go b/internal/coremsgs/en_config_descriptions.go
index 91c5f82ed..784862a18 100644
--- a/internal/coremsgs/en_config_descriptions.go
+++ b/internal/coremsgs/en_config_descriptions.go
@@ -70,24 +70,21 @@ var (
 
 	ConfigBlockchainEthereumEthconnectBatchSize    = ffc("config.blockchain.ethereum.ethconnect.batchSize", "The number of events Ethconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
 	ConfigBlockchainEthereumEthconnectBatchTimeout = ffc("config.blockchain.ethereum.ethconnect.batchTimeout", "How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream", i18n.TimeDurationType)
-	ConfigBlockchainEthereumEthconnectInstance     = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use fireflyContract[].address)", "Address "+i18n.StringType)
-	ConfigBlockchainEthereumEthconnectFromBlock    = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use fireflyContract[].fromBlock)", "Address "+i18n.StringType)
+	ConfigBlockchainEthereumEthconnectInstance     = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use namespace.predefined[].multiparty.contract[].location.address)", "Address "+i18n.StringType)
+	ConfigBlockchainEthereumEthconnectFromBlock    = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use namespace.predefined[].multiparty.contract[].location.firstEvent)", "Address "+i18n.StringType)
 	ConfigBlockchainEthereumEthconnectPrefixLong   = ffc("config.blockchain.ethereum.ethconnect.prefixLong", "The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect", i18n.StringType)
 	ConfigBlockchainEthereumEthconnectPrefixShort  = ffc("config.blockchain.ethereum.ethconnect.prefixShort", "The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect", i18n.StringType)
 	ConfigBlockchainEthereumEthconnectTopic        = ffc("config.blockchain.ethereum.ethconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single ethconnect", i18n.StringType)
 	ConfigBlockchainEthereumEthconnectURL          = ffc("config.blockchain.ethereum.ethconnect.url", "The URL of the Ethconnect instance", "URL "+i18n.StringType)
 	ConfigBlockchainEthereumEthconnectProxyURL     = ffc("config.blockchain.ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType)
 
-	ConfigBlockchainEthereumContractAddress   = ffc("config.blockchain.ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType)
-	ConfigBlockchainEthereumContractFromBlock = ffc("config.blockchain.ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType)
-
 	ConfigBlockchainEthereumFFTMURL      = ffc("config.blockchain.ethereum.fftm.url", "The URL of the FireFly Transaction Manager runtime, if enabled", i18n.StringType)
 	ConfigBlockchainEthereumFFTMProxyURL = ffc("config.blockchain.ethereum.fftm.proxy.url", "Optional HTTP proxy server to use when connecting to the Transaction Manager", i18n.StringType)
 
 	ConfigBlockchainFabricFabconnectBatchSize    = ffc("config.blockchain.fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
 	ConfigBlockchainFabricFabconnectBatchTimeout = ffc("config.blockchain.fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType)
-	ConfigBlockchainFabricFabconnectChaincode    = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)", i18n.StringType)
-	ConfigBlockchainFabricFabconnectChannel      = ffc("config.blockchain.fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions", i18n.StringType)
+	ConfigBlockchainFabricFabconnectChaincode    = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.chaincode)", i18n.StringType)
+	ConfigBlockchainFabricFabconnectChannel      = ffc("config.blockchain.fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions (deprecated - use namespace.predefined[].multiparty.contract[].location.channel)", i18n.StringType)
 	ConfigBlockchainFabricFabconnectPrefixLong   = ffc("config.blockchain.fabric.fabconnect.prefixLong", "The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigBlockchainFabricFabconnectPrefixShort  = ffc("config.blockchain.fabric.fabconnect.prefixShort", "The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigBlockchainFabricFabconnectSigner       = ffc("config.blockchain.fabric.fabconnect.signer", "The Fabric signing key to use when submitting transactions to Fabconnect", i18n.StringType)
@@ -95,9 +92,6 @@ var (
 	ConfigBlockchainFabricFabconnectURL          = ffc("config.blockchain.fabric.fabconnect.url", "The URL of the Fabconnect instance", "URL "+i18n.StringType)
 	ConfigBlockchainFabricFabconnectProxyURL     = ffc("config.blockchain.fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType)
 
-	ConfigBlockchainFabricContractChaincode = ffc("config.blockchain.fabric.fireflyContract[].chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType)
-	ConfigBlockchainFabricContractFromBlock = ffc("config.blockchain.fabric.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType)
-
 	ConfigPluginDatabase     = ffc("config.plugins.database", "The list of configured Database plugins", i18n.StringType)
 	ConfigPluginDatabaseName = ffc("config.plugins.database[].name", "The name of the Database plugin", i18n.StringType)
 	ConfigPluginDatabaseType = ffc("config.plugins.database[].type", "The type of the configured Database plugin", i18n.StringType)
@@ -143,25 +137,19 @@ var (
 	ConfigPluginBlockchainEthereumEthconnectURL          = ffc("config.plugins.blockchain[].ethereum.ethconnect.url", "The URL of the Ethconnect instance", "URL "+i18n.StringType)
 	ConfigPluginBlockchainEthereumEthconnectProxyURL     = ffc("config.plugins.blockchain[].ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType)
 
-	ConfigPluginBlockchainEthereumContractAddress   = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType)
-	ConfigPluginBlockchainEthereumContractFromBlock = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType)
-
 	ConfigPluginBlockchainEthereumFFTMURL      = ffc("config.plugins.blockchain[].ethereum.fftm.url", "The URL of the FireFly Transaction Manager runtime, if enabled", i18n.StringType)
 	ConfigPluginBlockchainEthereumFFTMProxyURL = ffc("config.plugins.blockchain[].ethereum.fftm.proxy.url", "Optional HTTP proxy server to use when connecting to the Transaction Manager", i18n.StringType)
 
 	ConfigPluginBlockchainFabricFabconnectBatchSize    = ffc("config.plugins.blockchain[].fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType)
 	ConfigPluginBlockchainFabricFabconnectBatchTimeout = ffc("config.plugins.blockchain[].fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType)
-	ConfigPluginBlockchainFabricFabconnectChaincode    = ffc("config.plugins.blockchain[].fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)", i18n.StringType)
-	ConfigPluginBlockchainFabricFabconnectChannel      = ffc("config.plugins.blockchain[].fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions", i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectPrefixLong   = ffc("config.plugins.blockchain[].fabric.fabconnect.prefixLong", "The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectPrefixShort  = ffc("config.plugins.blockchain[].fabric.fabconnect.prefixShort", "The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect", i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectSigner       = ffc("config.plugins.blockchain[].fabric.fabconnect.signer", "The Fabric signing key to use when submitting transactions to Fabconnect", i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectTopic        = ffc("config.plugins.blockchain[].fabric.fabconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single Fabconnect", i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectURL          = ffc("config.plugins.blockchain[].fabric.fabconnect.url", "The URL of the Fabconnect instance", "URL "+i18n.StringType)
 	ConfigPluginBlockchainFabricFabconnectProxyURL     = ffc("config.plugins.blockchain[].fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType)
-
-	ConfigPluginBlockchainFabricContractChaincode = ffc("config.plugins.blockchain[].fabric.fireflyContract[].chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType)
-	ConfigPluginBlockchainFabricContractFromBlock = ffc("config.plugins.blockchain[].fabric.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType)
+	ConfigPluginBlockchainFabricFabconnectChaincode    = ffc("config.plugins.blockchain[].fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)", i18n.StringType)
+	ConfigPluginBlockchainFabricFabconnectChannel      = ffc("config.plugins.blockchain[].fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions", i18n.StringType)
 
 	ConfigBroadcastBatchAgentTimeout = ffc("config.broadcast.batch.agentTimeout", "How long to keep around a batching agent for a sending identity before disposal", i18n.StringType)
 	ConfigBroadcastBatchPayloadLimit = ffc("config.broadcast.batch.payloadLimit", "The maximum payload size of a batch for broadcast messages", i18n.ByteSizeType)
@@ -259,17 +247,20 @@ var (
 	ConfigMetricsReadTimeout  = ffc("config.metrics.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType)
 	ConfigMetricsWriteTimeout = ffc("config.metrics.writeTimeout", "The maximum time to wait when writing to an HTTP connection", i18n.TimeDurationType)
 
-	ConfigNamespacesDefault               = ffc("config.namespaces.default", "The default namespace - must be in the predefined list", i18n.StringType)
-	ConfigNamespacesPredefined            = ffc("config.namespaces.predefined", "A list of namespaces to ensure exists, without requiring a broadcast from the network", "List "+i18n.StringType)
-	ConfigNamespacesPredefinedName        = ffc("config.namespaces.predefined[].name", "The name of the namespace (must be unique)", i18n.StringType)
-	ConfigNamespacesPredefinedDescription = ffc("config.namespaces.predefined[].description", "A description for the namespace", i18n.StringType)
-	ConfigNamespacesPredefinedPlugins     = ffc("config.namespaces.predefined[].plugins", "The list of plugins for this namespace", i18n.StringType)
-	ConfigNamespacesPredefinedRemoteName  = ffc("config.namespaces.predefined[].remoteName", "The namespace name to be sent in plugin calls, if it differs from namespace name", i18n.StringType)
-	ConfigNamespacesPredefinedDefaultKey  = ffc("config.namespaces.predefined[].defaultKey", "A default signing key for blockchain transactions within this namespace", i18n.StringType)
-	ConfigNamespacesMultipartyEnabled     = ffc("config.namespaces.predefined[].multiparty.enabled", "Enables multi-party mode for this namespace (defaults to true if an org name or key is configured, either here or at the root level)", i18n.BooleanType)
-	ConfigNamespacesMultipartyOrgName     = ffc("config.namespaces.predefined[].multiparty.org.name", "A short name for the local root organization within this namespace", i18n.StringType)
-	ConfigNamespacesMultipartyOrgDesc     = ffc("config.namespaces.predefined[].multiparty.org.description", "A description for the local root organization within this namespace", i18n.StringType)
-	ConfigNamespacesMultipartyOrgKey      = ffc("config.namespaces.predefined[].multiparty.org.key", "The signing key allocated to the root organization within this namespace", i18n.StringType)
+	ConfigNamespacesDefault                      = ffc("config.namespaces.default", "The default namespace - must be in the predefined list", i18n.StringType)
+	ConfigNamespacesPredefined                   = ffc("config.namespaces.predefined", "A list of namespaces to ensure exists, without requiring a broadcast from the network", "List "+i18n.StringType)
+	ConfigNamespacesPredefinedName               = ffc("config.namespaces.predefined[].name", "The name of the namespace (must be unique)", i18n.StringType)
+	ConfigNamespacesPredefinedDescription        = ffc("config.namespaces.predefined[].description", "A description for the namespace", i18n.StringType)
+	ConfigNamespacesPredefinedPlugins            = ffc("config.namespaces.predefined[].plugins", "The list of plugins for this namespace", i18n.StringType)
+	ConfigNamespacesPredefinedRemoteName         = ffc("config.namespaces.predefined[].remoteName", "The namespace name to be sent in plugin calls, if it differs from namespace name", i18n.StringType)
+	ConfigNamespacesPredefinedDefaultKey         = ffc("config.namespaces.predefined[].defaultKey", "A default signing key for blockchain transactions within this namespace", i18n.StringType)
+	ConfigNamespacesMultipartyEnabled            = ffc("config.namespaces.predefined[].multiparty.enabled", "Enables multi-party mode for this namespace (defaults to true if an org name or key is configured, either here or at the root level)", i18n.BooleanType)
+	ConfigNamespacesMultipartyOrgName            = ffc("config.namespaces.predefined[].multiparty.org.name", "A short name for the local root organization within this namespace", i18n.StringType)
+	ConfigNamespacesMultipartyOrgDesc            = ffc("config.namespaces.predefined[].multiparty.org.description", "A description for the local root organization within this namespace", i18n.StringType)
+	ConfigNamespacesMultipartyOrgKey             = ffc("config.namespaces.predefined[].multiparty.org.key", "The signing key allocated to the root organization within this namespace", i18n.StringType)
+	ConfigNamespacesMultipartyContract           = ffc("config.namespaces.predefined[].contract", "A list containing configuration for the multi-party blockchain contract", i18n.StringType)
+	ConfigNamespacesMultipartyContractFirstEvent = ffc("config.namespaces.predefined[].multiparty.contract[].firstEvent", "The first event the contract should process. Valid options are `oldest` or `newest`", i18n.StringType)
+	ConfigNamespacesMultipartyContractLocation   = ffc("config.namespaces.predefined[].multiparty.contract[].location", "A blockchain-specific contract location. For example, an Ethereum contract address, or a Fabric chaincode name and channe", i18n.StringType)
 
 	ConfigNodeDescription = ffc("config.node.description", "The description of this FireFly node", i18n.StringType)
 	ConfigNodeName        = ffc("config.node.name", "The name of this FireFly node", i18n.StringType)
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index 00943f765..9de452958 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -251,4 +251,5 @@ var (
 	MsgDefRejectedWrongAuthor             = ffe("FF10409", "Rejected %s '%s' - wrong author: %s")
 	MsgDefRejectedHashMismatch            = ffe("FF10410", "Rejected %s '%s' - hash mismatch: %s != %s")
 	MsgInvalidNamespaceUUID               = ffe("FF10411", "Expected 'namespace:' prefix on ID '%s'", 400)
+	MsgSubscriptionIDInvalid              = ffe("FF10412", "Invalid subscription ID: %s")
 )
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index f884bc79d..cfebd196f 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -374,19 +374,21 @@ var (
 	VerifierCreated   = ffm("Verifier.created", "The time this verifier was created on this node")
 
 	// Namespace field descriptions
-	NamespaceID                = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node")
-	NamespaceMessage           = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces")
-	NamespaceName              = ffm("Namespace.name", "The namespace name")
-	NamespaceDescription       = ffm("Namespace.description", "A description of the namespace")
-	NamespaceType              = ffm("Namespace.type", "The type of the namespace")
-	NamespaceCreated           = ffm("Namespace.created", "The time the namespace was created")
-	NamespaceContract          = ffm("Namespace.fireflyContract", "Info on the FireFly smart contract configured for this namespace")
-	FireFlyContractsActive     = ffm("FireFlyContracts.active", "The currently active FireFly smart contract")
-	FireFlyContractsTerminated = ffm("FireFlyContracts.terminated", "Previously-terminated FireFly smart contracts")
-	FireFlyContractIndex       = ffm("FireFlyContractInfo.index", "The index of this contract in the config file")
-	FireFlyContractFinalEvent  = ffm("FireFlyContractInfo.finalEvent", "The identifier for the final blockchain event received from this contract before termination")
-	FireFlyContractInfo        = ffm("FireFlyContractInfo.info", "Blockchain-specific info on the contract, such as its location on chain")
-	NetworkActionType          = ffm("NetworkAction.type", "The action to be performed")
+	NamespaceID                 = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node")
+	NamespaceMessage            = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces")
+	NamespaceName               = ffm("Namespace.name", "The namespace name")
+	NamespaceDescription        = ffm("Namespace.description", "A description of the namespace")
+	NamespaceType               = ffm("Namespace.type", "The type of the namespace")
+	NamespaceCreated            = ffm("Namespace.created", "The time the namespace was created")
+	NamespaceContract           = ffm("Namespace.fireflyContract", "Info on the FireFly smart contract configured for this namespace")
+	FireFlyContractsActive      = ffm("FireFlyContracts.active", "The currently active FireFly smart contract")
+	FireFlyContractsTerminated  = ffm("FireFlyContracts.terminated", "Previously-terminated FireFly smart contracts")
+	FireFlyContractIndex        = ffm("FireFlyContractInfo.index", "The index of this contract in the config file")
+	FireFlyContractFinalEvent   = ffm("FireFlyContractInfo.finalEvent", "The identifier for the final blockchain event received from this contract before termination")
+	FireFlyContractFirstEvent   = ffm("FireFlyContractInfo.firstEvent", "A blockchain specific string, such as a block number, to start listening from. The special strings 'oldest' and 'newest' are supported by all blockchain connectors")
+	FireFlyContractLocation     = ffm("FireFlyContractInfo.location", "A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel")
+	FireFlyContractSubscription = ffm("FireFlyContractInfo.subscription", "The backend identifier of the subscription for the FireFly BatchPin contract")
+	NetworkActionType           = ffm("NetworkAction.type", "The action to be performed")
 
 	// NodeStatus field descriptions
 	NodeNamespace  = ffm("NodeStatus.namespace", "The namespace that this status applies to")
diff --git a/internal/dataexchange/ffdx/dxevent.go b/internal/dataexchange/ffdx/dxevent.go
index 8020e3c73..304ad4aa3 100644
--- a/internal/dataexchange/ffdx/dxevent.go
+++ b/internal/dataexchange/ffdx/dxevent.go
@@ -45,19 +45,14 @@ type wsEvent struct {
 type dxEvent struct {
 	ffdx                *FFDX
 	id                  string
-	requestID           string
 	dxType              dataexchange.DXEventType
 	messageReceived     *dataexchange.MessageReceived
 	privateBlobReceived *dataexchange.PrivateBlobReceived
 	transferResult      *dataexchange.TransferResult
 }
 
-func (e *dxEvent) EventID() string {
-	return e.id
-}
-
 func (e *dxEvent) NamespacedID() string {
-	return e.requestID
+	return e.id
 }
 
 func (e *dxEvent) Type() dataexchange.DXEventType {
@@ -93,7 +88,7 @@ func (e *dxEvent) TransferResult() *dataexchange.TransferResult {
 
 func (h *FFDX) dispatchEvent(msg *wsEvent) {
 	var err error
-	e := &dxEvent{ffdx: h, id: msg.EventID, requestID: msg.RequestID}
+	e := &dxEvent{ffdx: h, id: msg.EventID}
 	switch msg.Type {
 	case messageFailed:
 		e.dxType = dataexchange.DXEventTypeTransferResult
diff --git a/internal/events/batch_pin_complete.go b/internal/events/batch_pin_complete.go
index 404ff9d8e..5c2eeb856 100644
--- a/internal/events/batch_pin_complete.go
+++ b/internal/events/batch_pin_complete.go
@@ -31,7 +31,7 @@ import (
 //
 // We must block here long enough to get the payload from the sharedstorage, persist the messages in the correct
 // sequence, and also persist all the data.
-func (em *eventManager) BatchPinComplete(bi blockchain.Plugin, batchPin *blockchain.BatchPin, signingKey *core.VerifierRef) error {
+func (em *eventManager) BatchPinComplete(batchPin *blockchain.BatchPin, signingKey *core.VerifierRef) error {
 	if batchPin.TransactionID == nil {
 		log.L(em.ctx).Errorf("Invalid BatchPin transaction - ID is nil")
 		return nil // move on
diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go
index 0ac33010d..d1b5325fc 100644
--- a/internal/events/event_manager.go
+++ b/internal/events/event_manager.go
@@ -38,6 +38,7 @@ import (
 	"github.com/hyperledger/firefly/internal/events/system"
 	"github.com/hyperledger/firefly/internal/identity"
 	"github.com/hyperledger/firefly/internal/metrics"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/privatemessaging"
 	"github.com/hyperledger/firefly/internal/shareddownload"
 	"github.com/hyperledger/firefly/internal/sysmessaging"
@@ -64,9 +65,9 @@ type EventManager interface {
 	WaitStop()
 
 	// Bound blockchain callbacks
-	BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error
+	BatchPinComplete(batch *blockchain.BatchPin, signingKey *core.VerifierRef) error
 	BlockchainEvent(event *blockchain.EventWithSubscription) error
-	BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error
+	BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error
 
 	// Bound dataexchange callbacks
 	DXEvent(dx dataexchange.Plugin, event dataexchange.DXEvent)
@@ -111,9 +112,10 @@ type eventManager struct {
 	metrics               metrics.Manager
 	chainListenerCache    *ccache.Cache
 	chainListenerCacheTTL time.Duration
+	multiparty            multiparty.Manager
 }
 
-func NewEventManager(ctx context.Context, ns string, ni sysmessaging.LocalNodeInfo, si sharedstorage.Plugin, di database.Plugin, bi blockchain.Plugin, im identity.Manager, dh definitions.DefinitionHandler, dm data.Manager, bm broadcast.Manager, pm privatemessaging.Manager, am assets.Manager, sd shareddownload.Manager, mm metrics.Manager, txHelper txcommon.Helper, transports map[string]events.Plugin) (EventManager, error) {
+func NewEventManager(ctx context.Context, ns string, ni sysmessaging.LocalNodeInfo, si sharedstorage.Plugin, di database.Plugin, bi blockchain.Plugin, im identity.Manager, dh definitions.DefinitionHandler, dm data.Manager, bm broadcast.Manager, pm privatemessaging.Manager, am assets.Manager, sd shareddownload.Manager, mm metrics.Manager, txHelper txcommon.Helper, transports map[string]events.Plugin, mp multiparty.Manager) (EventManager, error) {
 	if ni == nil || si == nil || di == nil || bi == nil || im == nil || dh == nil || dm == nil || bm == nil || pm == nil || am == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "EventManager")
 	}
@@ -133,6 +135,7 @@ func NewEventManager(ctx context.Context, ns string, ni sysmessaging.LocalNodeIn
 		messaging:      pm,
 		assets:         am,
 		sharedDownload: sd,
+		multiparty:     mp,
 		retry: retry.Retry{
 			InitialDelay: config.GetDuration(coreconfig.EventAggregatorRetryInitDelay),
 			MaximumDelay: config.GetDuration(coreconfig.EventAggregatorRetryMaxDelay),
diff --git a/internal/events/network_action.go b/internal/events/network_action.go
index 2a0d516d3..e3006c5ed 100644
--- a/internal/events/network_action.go
+++ b/internal/events/network_action.go
@@ -20,16 +20,17 @@ import (
 	"context"
 
 	"github.com/hyperledger/firefly-common/pkg/log"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
 )
 
-func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain.Event) error {
+func (em *eventManager) actionTerminate(mm multiparty.Manager, event *blockchain.Event) error {
 	namespace, err := em.database.GetNamespace(em.ctx, em.namespace)
 	if err != nil {
 		return err
 	}
-	if err := bi.TerminateContract(em.ctx, &namespace.Contracts, event); err != nil {
+	if err := mm.TerminateContract(em.ctx, &namespace.Contracts, event); err != nil {
 		return err
 	}
 	// Currently, a termination event is implied to apply to ALL namespaces
@@ -41,7 +42,7 @@ func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain.
 	})
 }
 
-func (em *eventManager) BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error {
+func (em *eventManager) BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error {
 	return em.retry.Do(em.ctx, "handle network action", func(attempt int) (retry bool, err error) {
 		// Verify that the action came from a registered root org
 		resolvedAuthor, err := em.identity.FindIdentityForVerifier(em.ctx, []core.IdentityType{core.IdentityTypeOrg}, signingKey)
@@ -58,7 +59,7 @@ func (em *eventManager) BlockchainNetworkAction(bi blockchain.Plugin, action str
 		}
 
 		if action == core.NetworkActionTerminate.String() {
-			err = em.actionTerminate(bi, event)
+			err = em.actionTerminate(em.multiparty, event)
 		} else {
 			log.L(em.ctx).Errorf("Ignoring unrecognized network action: %s", action)
 			return false, nil
diff --git a/internal/identity/identitymanager.go b/internal/identity/identitymanager.go
index d066831e6..26844067e 100644
--- a/internal/identity/identitymanager.go
+++ b/internal/identity/identitymanager.go
@@ -29,6 +29,7 @@ import (
 	"github.com/hyperledger/firefly/internal/coreconfig"
 	"github.com/hyperledger/firefly/internal/coremsgs"
 	"github.com/hyperledger/firefly/internal/data"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/hyperledger/firefly/pkg/database"
@@ -58,6 +59,7 @@ type identityManager struct {
 	database               database.Plugin
 	blockchain             blockchain.Plugin
 	data                   data.Manager
+	multiparty             multiparty.Manager
 	namespace              string
 	defaultKey             string
 	orgName                string
@@ -70,7 +72,7 @@ type identityManager struct {
 	signingKeyCache        *ccache.Cache
 }
 
-func NewIdentityManager(ctx context.Context, ns, defaultKey, orgName, orgKey string, di database.Plugin, bi blockchain.Plugin, dm data.Manager) (Manager, error) {
+func NewIdentityManager(ctx context.Context, ns, defaultKey, orgName, orgKey string, di database.Plugin, bi blockchain.Plugin, dm data.Manager, mp multiparty.Manager) (Manager, error) {
 	if di == nil || bi == nil || dm == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "IdentityManager")
 	}
@@ -79,6 +81,7 @@ func NewIdentityManager(ctx context.Context, ns, defaultKey, orgName, orgKey str
 		blockchain:         bi,
 		data:               dm,
 		namespace:          ns,
+		multiparty:         mp,
 		defaultKey:         defaultKey,
 		orgName:            orgName,
 		orgKey:             orgKey,
@@ -387,7 +390,7 @@ func (im *identityManager) cachedIdentityLookupByVerifierRef(ctx context.Context
 	if err != nil {
 		return nil, err
 	} else if verifier == nil {
-		if namespace != core.LegacySystemNamespace && im.blockchain.NetworkVersion() == 1 {
+		if namespace != core.LegacySystemNamespace && im.multiparty.GetNetworkVersion() == 1 {
 			// For V1 networks, fall back to LegacySystemNamespace for looking up identities
 			// This assumes that the system namespace shares a database with this manager's namespace!
 			return im.cachedIdentityLookupByVerifierRef(ctx, core.LegacySystemNamespace, verifierRef)
diff --git a/internal/multiparty/manager.go b/internal/multiparty/manager.go
new file mode 100644
index 000000000..60521d485
--- /dev/null
+++ b/internal/multiparty/manager.go
@@ -0,0 +1,154 @@
+// 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 (
+	"context"
+	"fmt"
+
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly-common/pkg/i18n"
+	"github.com/hyperledger/firefly-common/pkg/log"
+	"github.com/hyperledger/firefly/internal/coremsgs"
+	"github.com/hyperledger/firefly/pkg/blockchain"
+	"github.com/hyperledger/firefly/pkg/core"
+)
+
+type Manager interface {
+	// ConfigureContract initializes the subscription to the FireFly contract
+	// - Checks the provided contract info against the plugin's configuration, and updates it as needed
+	// - Initializes the contract info for performing BatchPin transactions, and initializes subscriptions for BatchPin events
+	ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error)
+
+	// TerminateContract marks the given event as the last one to be parsed on the current FireFly contract
+	// - Validates that the event came from the currently active FireFly contract
+	// - Re-initializes the plugin against the next configured FireFly contract
+	// - Updates the provided contract info to record the point of termination and the newly active contract
+	TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error)
+
+	// GetNetworkVersion returns the network version of the active FireFly contract
+	GetNetworkVersion() int
+
+	// SubmitBatchPin sequences a batch of message globally to all viewers of a given ledger
+	SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error
+
+	// SubmitNetworkAction writes a special "BatchPin" event which signals the plugin to take an action
+	SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType) error
+
+	core.Named
+}
+
+type Contract struct {
+	Location   *fftypes.JSONAny
+	FirstEvent string
+}
+
+type multipartyManager struct {
+	ctx            context.Context
+	blockchain     blockchain.Plugin
+	contracts      []Contract
+	namespace      string
+	activeContract struct {
+		location       *fftypes.JSONAny
+		firstEvent     string
+		networkVersion int
+		subscription   string
+	}
+}
+
+func NewMultipartyManager(ctx context.Context, ns string, contracts []Contract, bi blockchain.Plugin) Manager {
+	return &multipartyManager{
+		ctx:        ctx,
+		namespace:  ns,
+		contracts:  contracts,
+		blockchain: bi,
+	}
+}
+
+func (mm *multipartyManager) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) {
+	log.L(ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index)
+	location, firstEvent, err := mm.resolveFireFlyContract(ctx, contracts.Active.Index)
+	if err != nil {
+		return err
+	}
+	version, err := mm.blockchain.GetNetworkVersion(ctx, location)
+	if err != nil {
+		return err
+	}
+
+	subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace, location, firstEvent)
+	if err == nil {
+		mm.activeContract.location = location
+		mm.activeContract.firstEvent = firstEvent
+		mm.activeContract.networkVersion = version
+		mm.activeContract.subscription = subID
+		contracts.Active = core.FireFlyContractInfo{
+			Location:     location,
+			FirstEvent:   firstEvent,
+			Subscription: subID,
+		}
+	}
+	return err
+}
+
+func (mm *multipartyManager) resolveFireFlyContract(ctx context.Context, contractIndex int) (location *fftypes.JSONAny, firstEvent string, err error) {
+	if len(mm.contracts) > 0 {
+		if contractIndex >= len(mm.contracts) {
+			return nil, "", i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("%s.multiparty.contracts[%d]", mm.namespace, contractIndex))
+		}
+		active := mm.contracts[contractIndex]
+		location = active.Location
+		firstEvent = active.FirstEvent
+	} else {
+		// handle deprecated config here
+		location, firstEvent, err = mm.blockchain.GetAndConvertDeprecatedContractConfig(ctx)
+		if err != nil {
+			return nil, "", err
+		}
+	}
+
+	return location, firstEvent, err
+}
+
+func (mm *multipartyManager) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) {
+	// TODO: Investigate if it better to consolidate DB termination here
+	log.L(ctx).Infof("Processing termination of contract at index %d", contracts.Active.Index)
+	contracts.Active.FinalEvent = termination.ProtocolID
+	contracts.Terminated = append(contracts.Terminated, contracts.Active)
+	contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1}
+	err = mm.blockchain.RemoveFireflySubscription(ctx, mm.activeContract.subscription)
+	if err != nil {
+		return err
+	}
+	return mm.ConfigureContract(ctx, contracts)
+}
+
+func (mm *multipartyManager) GetNetworkVersion() int {
+	return mm.activeContract.networkVersion
+}
+
+func (mm *multipartyManager) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType) error {
+	return mm.blockchain.SubmitNetworkAction(ctx, nsOpID, signingKey, action, mm.activeContract.location)
+}
+
+func (mm *multipartyManager) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error {
+	return mm.blockchain.SubmitBatchPin(ctx, nsOpID, signingKey, batch, mm.activeContract.location)
+}
+
+func (mm *multipartyManager) Name() string {
+	return mm.blockchain.Name()
+}
diff --git a/internal/namespace/config.go b/internal/namespace/config.go
index 762f71aca..b6da65d2d 100644
--- a/internal/namespace/config.go
+++ b/internal/namespace/config.go
@@ -19,11 +19,13 @@ package namespace
 import (
 	"github.com/hyperledger/firefly-common/pkg/config"
 	"github.com/hyperledger/firefly/internal/coreconfig"
+	"github.com/hyperledger/firefly/pkg/core"
 )
 
 const (
 	// NamespacePredefined is the list of pre-defined namespaces
-	NamespacePredefined = "predefined"
+	NamespacePredefined         = "predefined"
+	NamespaceMultipartyContract = "contract"
 )
 
 var (
@@ -37,10 +39,17 @@ func InitConfig(withDefaults bool) {
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceRemoteName)
 	namespacePredefined.AddKnownKey(coreconfig.NamespacePlugins)
 	namespacePredefined.AddKnownKey(coreconfig.NamespaceDefaultKey)
-	namespacePredefined.AddKnownKey(coreconfig.NamespaceMultipartyEnabled)
-	namespacePredefined.AddKnownKey(coreconfig.NamespaceMultipartyOrgName)
-	namespacePredefined.AddKnownKey(coreconfig.NamespaceMultipartyOrgDescription)
-	namespacePredefined.AddKnownKey(coreconfig.NamespaceMultipartyOrgKey)
+
+	multipartyConf := namespacePredefined.SubSection(coreconfig.NamespaceMultiparty)
+	multipartyConf.AddKnownKey(coreconfig.NamespaceMultipartyEnabled)
+	multipartyConf.AddKnownKey(coreconfig.NamespaceMultipartyOrgName)
+	multipartyConf.AddKnownKey(coreconfig.NamespaceMultipartyOrgDescription)
+	multipartyConf.AddKnownKey(coreconfig.NamespaceMultipartyOrgKey)
+
+	contractConf := multipartyConf.SubArray(coreconfig.NamespaceMultipartyContract)
+	contractConf.AddKnownKey(coreconfig.NamespaceMultipartyContractFirstEvent, core.SubOptsFirstEventOldest)
+	contractConf.AddKnownKey(coreconfig.NamespaceMultipartyContractLocation)
+
 	if withDefaults {
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceName, "default")
 		namespaceConfig.AddKnownKey(NamespacePredefined+".0."+coreconfig.NamespaceDescription, "Default predefined namespace")
diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go
index 5081d157e..6c93271c4 100644
--- a/internal/namespace/manager.go
+++ b/internal/namespace/manager.go
@@ -18,9 +18,11 @@ package namespace
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 
 	"github.com/hyperledger/firefly-common/pkg/config"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly-common/pkg/i18n"
 	"github.com/hyperledger/firefly-common/pkg/log"
 	"github.com/hyperledger/firefly/internal/blockchain/bifactory"
@@ -32,6 +34,7 @@ import (
 	"github.com/hyperledger/firefly/internal/events/system"
 	"github.com/hyperledger/firefly/internal/identity/iifactory"
 	"github.com/hyperledger/firefly/internal/metrics"
+	multipartyManager "github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/orchestrator"
 	"github.com/hyperledger/firefly/internal/sharedstorage/ssfactory"
 	"github.com/hyperledger/firefly/internal/spievents"
@@ -184,7 +187,7 @@ func (nm *namespaceManager) Init(ctx context.Context, cancelCtx context.CancelFu
 		}
 
 		// If the default namespace is a multiparty V1 namespace, insert the legacy ff_system namespace
-		if name == defaultNS && ns.config.Multiparty.Enabled && ns.plugins.Blockchain.Plugin.NetworkVersion() == 1 {
+		if name == defaultNS && ns.config.Multiparty.Enabled && ns.orchestrator.MultiParty().GetNetworkVersion() == 1 {
 			systemNS = &namespace{}
 			*systemNS = *ns
 			if err := nm.initNamespace(core.LegacySystemNamespace, systemNS); err != nil {
@@ -617,20 +620,21 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 		return nil, i18n.NewError(ctx, coremsgs.MsgFFSystemReservedName, core.LegacySystemNamespace)
 	}
 
+	multipartyConf := conf.SubSection(coreconfig.NamespaceMultiparty)
 	// If any multiparty org information is configured (here or at the root), assume multiparty mode by default
-	orgName := conf.GetString(coreconfig.NamespaceMultipartyOrgName)
+	orgName := multipartyConf.GetString(coreconfig.NamespaceMultipartyOrgName)
 	if orgName == "" {
 		orgName = config.GetString(coreconfig.OrgName)
 	}
-	orgKey := conf.GetString(coreconfig.NamespaceMultipartyOrgKey)
+	orgKey := multipartyConf.GetString(coreconfig.NamespaceMultipartyOrgKey)
 	if orgKey == "" {
 		orgKey = config.GetString(coreconfig.OrgKey)
 	}
-	orgDesc := conf.GetString(coreconfig.NamespaceMultipartyOrgDescription)
+	orgDesc := multipartyConf.GetString(coreconfig.NamespaceMultipartyOrgDescription)
 	if orgDesc == "" {
 		orgDesc = config.GetString(coreconfig.OrgDescription)
 	}
-	multiparty := conf.Get(coreconfig.NamespaceMultipartyEnabled)
+	multiparty := multipartyConf.Get(coreconfig.NamespaceMultipartyEnabled)
 	if multiparty == nil {
 		multiparty = orgName != "" || orgKey != ""
 	}
@@ -670,10 +674,30 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	var p *orchestrator.Plugins
 	var err error
 	if multiparty.(bool) {
+		contractsConf := multipartyConf.SubArray(coreconfig.NamespaceMultipartyContract)
+		contractConfArraySize := contractsConf.ArraySize()
+		contracts := make([]multipartyManager.Contract, contractConfArraySize)
+
+		for i := 0; i < contractConfArraySize; i++ {
+			conf := contractsConf.ArrayEntry(i)
+			locationObject := conf.GetObject(coreconfig.NamespaceMultipartyContractLocation)
+			b, err := json.Marshal(locationObject)
+			if err != nil {
+				return nil, err
+			}
+			location := fftypes.JSONAnyPtrBytes(b)
+			contract := multipartyManager.Contract{
+				Location:   location,
+				FirstEvent: conf.GetString(coreconfig.NamespaceMultipartyContractFirstEvent),
+			}
+			contracts[i] = contract
+		}
+
 		config.Multiparty.Enabled = true
 		config.Multiparty.OrgName = orgName
 		config.Multiparty.OrgKey = orgKey
 		config.Multiparty.OrgDesc = orgDesc
+		config.Multiparty.Contracts = contracts
 		p, err = nm.validateMultiPartyConfig(ctx, name, plugins)
 	} else {
 		p, err = nm.validateGatewayConfig(ctx, name, plugins)
diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go
index a966830ba..43fe81ba1 100644
--- a/internal/orchestrator/bound_callbacks.go
+++ b/internal/orchestrator/bound_callbacks.go
@@ -19,6 +19,7 @@ package orchestrator
 import (
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/internal/events"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/operations"
 	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
@@ -28,11 +29,11 @@ import (
 )
 
 type boundCallbacks struct {
-	bi blockchain.Plugin
-	dx dataexchange.Plugin
-	ss sharedstorage.Plugin
-	ei events.EventManager
-	om operations.Manager
+	dx         dataexchange.Plugin
+	ss         sharedstorage.Plugin
+	ei         events.EventManager
+	om         operations.Manager
+	multiparty multiparty.Manager
 }
 
 func (bc *boundCallbacks) BlockchainOpUpdate(plugin blockchain.Plugin, nsOpID string, txState blockchain.TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
@@ -56,11 +57,11 @@ func (bc *boundCallbacks) TokenOpUpdate(plugin tokens.Plugin, nsOpID string, txS
 }
 
 func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKey *core.VerifierRef) error {
-	return bc.ei.BatchPinComplete(bc.bi, batch, signingKey)
+	return bc.ei.BatchPinComplete(batch, signingKey)
 }
 
 func (bc *boundCallbacks) BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error {
-	return bc.ei.BlockchainNetworkAction(bc.bi, action, event, signingKey)
+	return bc.ei.BlockchainNetworkAction(action, event, signingKey)
 }
 
 func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) {
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index c535abd5b..e0c90ca7d 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -33,6 +33,7 @@ import (
 	"github.com/hyperledger/firefly/internal/events"
 	"github.com/hyperledger/firefly/internal/identity"
 	"github.com/hyperledger/firefly/internal/metrics"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/networkmap"
 	"github.com/hyperledger/firefly/internal/operations"
 	"github.com/hyperledger/firefly/internal/privatemessaging"
@@ -58,6 +59,7 @@ type Orchestrator interface {
 	BatchManager() batch.Manager
 	Broadcast() broadcast.Manager
 	Contracts() contracts.Manager
+	MultiParty() multiparty.Manager
 	Data() data.Manager
 	Events() events.EventManager
 	NetworkMap() networkmap.Manager
@@ -158,10 +160,11 @@ type Plugins struct {
 type Config struct {
 	DefaultKey string
 	Multiparty struct {
-		Enabled bool
-		OrgName string
-		OrgDesc string
-		OrgKey  string
+		Enabled   bool
+		OrgName   string
+		OrgDesc   string
+		OrgKey    string
+		Contracts []multiparty.Contract
 	}
 }
 
@@ -190,6 +193,7 @@ type orchestrator struct {
 	operations     operations.Manager
 	sharedDownload shareddownload.Manager
 	txHelper       txcommon.Helper
+	multiparty     multiparty.Manager
 }
 
 func NewOrchestrator(ns string, config Config, plugins Plugins, metrics metrics.Manager) Orchestrator {
@@ -210,7 +214,7 @@ func (or *orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc)
 		err = or.initComponents(or.ctx)
 	}
 	// Bind together the blockchain interface callbacks, with the events manager
-	or.bc.bi = or.plugins.Blockchain.Plugin
+	or.bc.multiparty = or.multiparty
 	or.bc.ei = or.events
 	or.bc.dx = or.plugins.DataExchange.Plugin
 	or.bc.ss = or.plugins.SharedStorage.Plugin
@@ -252,7 +256,7 @@ func (or *orchestrator) Start() (err error) {
 				Created: fftypes.Now(),
 			}
 		}
-		err = or.blockchain().ConfigureContract(or.ctx, &ns.Contracts)
+		err = or.multiparty.ConfigureContract(or.ctx, &ns.Contracts)
 	}
 	if err == nil {
 		err = or.blockchain().Start()
@@ -352,6 +356,10 @@ func (or *orchestrator) Operations() operations.Manager {
 	return or.operations
 }
 
+func (or *orchestrator) MultiParty() multiparty.Manager {
+	return or.multiparty
+}
+
 func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
 	or.plugins.Database.Plugin.SetHandler(or)
 	or.plugins.Blockchain.Plugin.SetHandler(&or.bc)
@@ -383,6 +391,10 @@ func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
 
 func (or *orchestrator) initComponents(ctx context.Context) (err error) {
 
+	if or.multiparty == nil {
+		or.multiparty = multiparty.NewMultipartyManager(or.ctx, or.namespace, or.config.Multiparty.Contracts, or.blockchain())
+	}
+
 	if or.data == nil {
 		or.data, err = data.NewDataManager(ctx, or.namespace, or.database(), or.sharedstorage(), or.dataexchange())
 		if err != nil {
@@ -395,7 +407,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) {
 	}
 
 	if or.identity == nil {
-		or.identity, err = identity.NewIdentityManager(ctx, or.namespace, or.config.DefaultKey, or.config.Multiparty.OrgName, or.config.Multiparty.OrgKey, or.database(), or.blockchain(), or.data)
+		or.identity, err = identity.NewIdentityManager(ctx, or.namespace, or.config.DefaultKey, or.config.Multiparty.OrgName, or.config.Multiparty.OrgKey, or.database(), or.blockchain(), or.data, or.multiparty)
 		if err != nil {
 			return err
 		}
@@ -417,7 +429,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) {
 	or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.namespace, or.database(), or.data)
 
 	if or.batchpin == nil {
-		if or.batchpin, err = batchpin.NewBatchPinSubmitter(ctx, or.namespace, or.database(), or.identity, or.blockchain(), or.metrics, or.operations); err != nil {
+		if or.batchpin, err = batchpin.NewBatchPinSubmitter(ctx, or.namespace, or.database(), or.identity, or.multiparty, or.metrics, or.operations); err != nil {
 			return err
 		}
 	}
@@ -463,7 +475,7 @@ func (or *orchestrator) initComponents(ctx context.Context) (err error) {
 	}
 
 	if or.events == nil {
-		or.events, err = events.NewEventManager(ctx, or.namespace, or, or.sharedstorage(), or.database(), or.blockchain(), or.identity, or.definitions, or.data, or.broadcast, or.messaging, or.assets, or.sharedDownload, or.metrics, or.txHelper, or.plugins.Events)
+		or.events, err = events.NewEventManager(ctx, or.namespace, or, or.sharedstorage(), or.database(), or.blockchain(), or.identity, or.definitions, or.data, or.broadcast, or.messaging, or.assets, or.sharedDownload, or.metrics, or.txHelper, or.plugins.Events, or.multiparty)
 		if err != nil {
 			return err
 		}
@@ -495,5 +507,5 @@ func (or *orchestrator) SubmitNetworkAction(ctx context.Context, action *core.Ne
 		Namespace: or.namespace,
 		ID:        fftypes.NewUUID(),
 	}
-	return or.blockchain().SubmitNetworkAction(ctx, po.NamespacedIDString(), key, action.Type)
+	return or.multiparty.SubmitNetworkAction(ctx, po.NamespacedIDString(), key, action.Type)
 }
diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go
index c2e599dd4..bc6cf7a69 100644
--- a/mocks/blockchainmocks/plugin.go
+++ b/mocks/blockchainmocks/plugin.go
@@ -36,6 +36,27 @@ func (_m *Plugin) AddContractListener(ctx context.Context, subscription *core.Co
 	return r0
 }
 
+// AddFireflySubscription provides a mock function with given fields: ctx, namespace, location, firstEvent
+func (_m *Plugin) AddFireflySubscription(ctx context.Context, namespace string, location *fftypes.JSONAny, firstEvent string) (string, error) {
+	ret := _m.Called(ctx, namespace, location, firstEvent)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.JSONAny, string) string); ok {
+		r0 = rf(ctx, namespace, location, firstEvent)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.JSONAny, string) error); ok {
+		r1 = rf(ctx, namespace, location, firstEvent)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // Capabilities provides a mock function with given fields:
 func (_m *Plugin) Capabilities() *blockchain.Capabilities {
 	ret := _m.Called()
@@ -52,20 +73,6 @@ func (_m *Plugin) Capabilities() *blockchain.Capabilities {
 	return r0
 }
 
-// ConfigureContract provides a mock function with given fields: ctx, contracts
-func (_m *Plugin) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) error {
-	ret := _m.Called(ctx, contracts)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts) error); ok {
-		r0 = rf(ctx, contracts)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
 // DeleteContractListener provides a mock function with given fields: ctx, subscription
 func (_m *Plugin) DeleteContractListener(ctx context.Context, subscription *core.ContractListener) error {
 	ret := _m.Called(ctx, subscription)
@@ -117,6 +124,36 @@ func (_m *Plugin) GenerateFFI(ctx context.Context, generationRequest *core.FFIGe
 	return r0, r1
 }
 
+// GetAndConvertDeprecatedContractConfig provides a mock function with given fields: ctx
+func (_m *Plugin) GetAndConvertDeprecatedContractConfig(ctx context.Context) (*fftypes.JSONAny, string, error) {
+	ret := _m.Called(ctx)
+
+	var r0 *fftypes.JSONAny
+	if rf, ok := ret.Get(0).(func(context.Context) *fftypes.JSONAny); ok {
+		r0 = rf(ctx)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*fftypes.JSONAny)
+		}
+	}
+
+	var r1 string
+	if rf, ok := ret.Get(1).(func(context.Context) string); ok {
+		r1 = rf(ctx)
+	} else {
+		r1 = ret.Get(1).(string)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context) error); ok {
+		r2 = rf(ctx)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
 // GetFFIParamValidator provides a mock function with given fields: ctx
 func (_m *Plugin) GetFFIParamValidator(ctx context.Context) (core.FFIParamValidator, error) {
 	ret := _m.Called(ctx)
@@ -140,6 +177,27 @@ func (_m *Plugin) GetFFIParamValidator(ctx context.Context) (core.FFIParamValida
 	return r0, r1
 }
 
+// GetNetworkVersion provides a mock function with given fields: ctx, location
+func (_m *Plugin) GetNetworkVersion(ctx context.Context, location *fftypes.JSONAny) (int, error) {
+	ret := _m.Called(ctx, location)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *fftypes.JSONAny) int); ok {
+		r0 = rf(ctx, location)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *fftypes.JSONAny) error); ok {
+		r1 = rf(ctx, location)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // Init provides a mock function with given fields: ctx, _a1, _a2
 func (_m *Plugin) Init(ctx context.Context, _a1 config.Section, _a2 metrics.Manager) error {
 	ret := _m.Called(ctx, _a1, _a2)
@@ -187,20 +245,6 @@ func (_m *Plugin) Name() string {
 	return r0
 }
 
-// NetworkVersion provides a mock function with given fields:
-func (_m *Plugin) NetworkVersion() int {
-	ret := _m.Called()
-
-	var r0 int
-	if rf, ok := ret.Get(0).(func() int); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(int)
-	}
-
-	return r0
-}
-
 // NormalizeContractLocation provides a mock function with given fields: ctx, location
 func (_m *Plugin) NormalizeContractLocation(ctx context.Context, location *fftypes.JSONAny) (*fftypes.JSONAny, error) {
 	ret := _m.Called(ctx, location)
@@ -273,13 +317,13 @@ func (_m *Plugin) SetHandler(handler blockchain.Callbacks) {
 	_m.Called(handler)
 }
 
-// Start provides a mock function with given fields:
-func (_m *Plugin) Start() error {
-	ret := _m.Called()
+// RemoveFireflySubscription provides a mock function with given fields: ctx, subID
+func (_m *Plugin) RemoveFireflySubscription(ctx context.Context, subID string) error {
+	ret := _m.Called(ctx, subID)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func() error); ok {
-		r0 = rf()
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, subID)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -287,13 +331,13 @@ func (_m *Plugin) Start() error {
 	return r0
 }
 
-// SubmitBatchPin provides a mock function with given fields: ctx, nsOpID, signingKey, batch
-func (_m *Plugin) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error {
-	ret := _m.Called(ctx, nsOpID, signingKey, batch)
+// Start provides a mock function with given fields:
+func (_m *Plugin) Start() error {
+	ret := _m.Called()
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, *blockchain.BatchPin) error); ok {
-		r0 = rf(ctx, nsOpID, signingKey, batch)
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -301,13 +345,13 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey
 	return r0
 }
 
-// SubmitNetworkAction provides a mock function with given fields: ctx, nsOpID, signingKey, action
-func (_m *Plugin) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action fftypes.FFEnum) error {
-	ret := _m.Called(ctx, nsOpID, signingKey, action)
+// SubmitBatchPin provides a mock function with given fields: ctx, nsOpID, signingKey, batch, location
+func (_m *Plugin) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin, location *fftypes.JSONAny) error {
+	ret := _m.Called(ctx, nsOpID, signingKey, batch, location)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, string, fftypes.FFEnum) error); ok {
-		r0 = rf(ctx, nsOpID, signingKey, action)
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, *blockchain.BatchPin, *fftypes.JSONAny) error); ok {
+		r0 = rf(ctx, nsOpID, signingKey, batch, location)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -315,13 +359,13 @@ func (_m *Plugin) SubmitNetworkAction(ctx context.Context, nsOpID string, signin
 	return r0
 }
 
-// TerminateContract provides a mock function with given fields: ctx, contracts, termination
-func (_m *Plugin) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) error {
-	ret := _m.Called(ctx, contracts, termination)
+// SubmitNetworkAction provides a mock function with given fields: ctx, nsOpID, signingKey, action, location
+func (_m *Plugin) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action fftypes.FFEnum, location *fftypes.JSONAny) error {
+	ret := _m.Called(ctx, nsOpID, signingKey, action, location)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts, *blockchain.Event) error); ok {
-		r0 = rf(ctx, contracts, termination)
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, fftypes.FFEnum, *fftypes.JSONAny) error); ok {
+		r0 = rf(ctx, nsOpID, signingKey, action, location)
 	} else {
 		r0 = ret.Error(0)
 	}
diff --git a/mocks/dataexchangemocks/dx_event.go b/mocks/dataexchangemocks/dx_event.go
index d251dd534..f2cc489e9 100644
--- a/mocks/dataexchangemocks/dx_event.go
+++ b/mocks/dataexchangemocks/dx_event.go
@@ -22,20 +22,6 @@ func (_m *DXEvent) AckWithManifest(manifest string) {
 	_m.Called(manifest)
 }
 
-// EventID provides a mock function with given fields:
-func (_m *DXEvent) EventID() string {
-	ret := _m.Called()
-
-	var r0 string
-	if rf, ok := ret.Get(0).(func() string); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(string)
-	}
-
-	return r0
-}
-
 // MessageReceived provides a mock function with given fields:
 func (_m *DXEvent) MessageReceived() *dataexchange.MessageReceived {
 	ret := _m.Called()
diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go
index a9804dc84..f309d3451 100644
--- a/mocks/eventmocks/event_manager.go
+++ b/mocks/eventmocks/event_manager.go
@@ -41,13 +41,13 @@ func (_m *EventManager) AddSystemEventListener(ns string, el system.EventListene
 	return r0
 }
 
-// BatchPinComplete provides a mock function with given fields: bi, batch, signingKey
-func (_m *EventManager) BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error {
-	ret := _m.Called(bi, batch, signingKey)
+// BatchPinComplete provides a mock function with given fields: batch, signingKey
+func (_m *EventManager) BatchPinComplete(batch *blockchain.BatchPin, signingKey *core.VerifierRef) error {
+	ret := _m.Called(batch, signingKey)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(blockchain.Plugin, *blockchain.BatchPin, *core.VerifierRef) error); ok {
-		r0 = rf(bi, batch, signingKey)
+	if rf, ok := ret.Get(0).(func(*blockchain.BatchPin, *core.VerifierRef) error); ok {
+		r0 = rf(batch, signingKey)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -69,13 +69,13 @@ func (_m *EventManager) BlockchainEvent(event *blockchain.EventWithSubscription)
 	return r0
 }
 
-// BlockchainNetworkAction provides a mock function with given fields: bi, action, event, signingKey
-func (_m *EventManager) BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error {
-	ret := _m.Called(bi, action, event, signingKey)
+// BlockchainNetworkAction provides a mock function with given fields: action, event, signingKey
+func (_m *EventManager) BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error {
+	ret := _m.Called(action, event, signingKey)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(blockchain.Plugin, string, *blockchain.Event, *core.VerifierRef) error); ok {
-		r0 = rf(bi, action, event, signingKey)
+	if rf, ok := ret.Get(0).(func(string, *blockchain.Event, *core.VerifierRef) error); ok {
+		r0 = rf(action, event, signingKey)
 	} else {
 		r0 = ret.Error(0)
 	}
diff --git a/mocks/multipartymocks/manager.go b/mocks/multipartymocks/manager.go
new file mode 100644
index 000000000..154835ed5
--- /dev/null
+++ b/mocks/multipartymocks/manager.go
@@ -0,0 +1,104 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package multipartymocks
+
+import (
+	context "context"
+
+	blockchain "github.com/hyperledger/firefly/pkg/blockchain"
+
+	core "github.com/hyperledger/firefly/pkg/core"
+
+	fftypes "github.com/hyperledger/firefly-common/pkg/fftypes"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Manager is an autogenerated mock type for the Manager type
+type Manager struct {
+	mock.Mock
+}
+
+// ConfigureContract provides a mock function with given fields: ctx, contracts
+func (_m *Manager) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) error {
+	ret := _m.Called(ctx, contracts)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts) error); ok {
+		r0 = rf(ctx, contracts)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// GetNetworkVersion provides a mock function with given fields:
+func (_m *Manager) GetNetworkVersion() int {
+	ret := _m.Called()
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func() int); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	return r0
+}
+
+// Name provides a mock function with given fields:
+func (_m *Manager) Name() string {
+	ret := _m.Called()
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	return r0
+}
+
+// SubmitBatchPin provides a mock function with given fields: ctx, nsOpID, signingKey, batch
+func (_m *Manager) SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *blockchain.BatchPin) error {
+	ret := _m.Called(ctx, nsOpID, signingKey, batch)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, *blockchain.BatchPin) error); ok {
+		r0 = rf(ctx, nsOpID, signingKey, batch)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// SubmitNetworkAction provides a mock function with given fields: ctx, nsOpID, signingKey, action
+func (_m *Manager) SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action fftypes.FFEnum) error {
+	ret := _m.Called(ctx, nsOpID, signingKey, action)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, fftypes.FFEnum) error); ok {
+		r0 = rf(ctx, nsOpID, signingKey, action)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// TerminateContract provides a mock function with given fields: ctx, contracts, termination
+func (_m *Manager) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) error {
+	ret := _m.Called(ctx, contracts, termination)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts, *blockchain.Event) error); ok {
+		r0 = rf(ctx, contracts, termination)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go
index 41a12ca97..6e0e4140e 100644
--- a/mocks/orchestratormocks/orchestrator.go
+++ b/mocks/orchestratormocks/orchestrator.go
@@ -22,6 +22,8 @@ import (
 
 	mock "github.com/stretchr/testify/mock"
 
+	multiparty "github.com/hyperledger/firefly/internal/multiparty"
+
 	networkmap "github.com/hyperledger/firefly/internal/networkmap"
 
 	operations "github.com/hyperledger/firefly/internal/operations"
@@ -1107,6 +1109,22 @@ func (_m *Orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc)
 	return r0
 }
 
+// MultiParty provides a mock function with given fields:
+func (_m *Orchestrator) MultiParty() multiparty.Manager {
+	ret := _m.Called()
+
+	var r0 multiparty.Manager
+	if rf, ok := ret.Get(0).(func() multiparty.Manager); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(multiparty.Manager)
+		}
+	}
+
+	return r0
+}
+
 // NetworkMap provides a mock function with given fields:
 func (_m *Orchestrator) NetworkMap() networkmap.Manager {
 	ret := _m.Called()
diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go
index 84ce9caaf..90580c45f 100644
--- a/pkg/blockchain/plugin.go
+++ b/pkg/blockchain/plugin.go
@@ -38,17 +38,6 @@ type Plugin interface {
 	// SetHandler registers a handler to receive callbacks
 	SetHandler(handler Callbacks)
 
-	// ConfigureContract initializes the subscription to the FireFly contract
-	// - Checks the provided contract info against the plugin's configuration, and updates it as needed
-	// - Initializes the contract info for performing BatchPin transactions, and initializes subscriptions for BatchPin events
-	ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error)
-
-	// TerminateContract marks the given event as the last one to be parsed on the current FireFly contract
-	// - Validates that the event came from the currently active FireFly contract
-	// - Re-initializes the plugin against the next configured FireFly contract
-	// - Updates the provided contract info to record the point of termination and the newly active contract
-	TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *Event) (err error)
-
 	// Blockchain interface must not deliver any events until start is called
 	Start() error
 
@@ -64,10 +53,10 @@ type Plugin interface {
 	NormalizeSigningKey(ctx context.Context, keyRef string) (string, error)
 
 	// SubmitBatchPin sequences a batch of message globally to all viewers of a given ledger
-	SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *BatchPin) error
+	SubmitBatchPin(ctx context.Context, nsOpID string, signingKey string, batch *BatchPin, location *fftypes.JSONAny) error
 
 	// SubmitNetworkAction writes a special "BatchPin" event which signals the plugin to take an action
-	SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType) error
+	SubmitNetworkAction(ctx context.Context, nsOpID string, signingKey string, action core.NetworkActionType, location *fftypes.JSONAny) error
 
 	// InvokeContract submits a new transaction to be executed by custom on-chain logic
 	InvokeContract(ctx context.Context, nsOpID string, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}, options map[string]interface{}) error
@@ -93,8 +82,17 @@ type Plugin interface {
 	// GenerateEventSignature generates a strigified signature for the event, incorporating any fields significant to identifying the event as unique
 	GenerateEventSignature(ctx context.Context, event *core.FFIEventDefinition) string
 
-	// NetworkVersion returns the version of the network rules being used by this plugin
-	NetworkVersion() int
+	// GetNetworkVersion queries the provided contract to get the network version
+	GetNetworkVersion(ctx context.Context, location *fftypes.JSONAny) (int, error)
+
+	// GetAndConvertDeprecatedContractConfig converts the deprecated ethconnect config to a location object
+	GetAndConvertDeprecatedContractConfig(ctx context.Context) (location *fftypes.JSONAny, fromBlock string, err error)
+
+	// AddFireflySubscription creates a FireFly BatchPin subscription for the provided location
+	AddFireflySubscription(ctx context.Context, namespace string, location *fftypes.JSONAny, firstEvent string) (string, error)
+
+	// RemoveFireFlySubscription removes the provided FireFly subscription
+	RemoveFireflySubscription(ctx context.Context, subID string) error
 }
 
 const FireFlyActionPrefix = "firefly:"
diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go
index 1704e9c2c..8593bc04c 100644
--- a/pkg/core/namespace.go
+++ b/pkg/core/namespace.go
@@ -56,9 +56,11 @@ type FireFlyContracts struct {
 }
 
 type FireFlyContractInfo struct {
-	Index      int                `ffstruct:"FireFlyContractInfo" json:"index"`
-	FinalEvent string             `ffstruct:"FireFlyContractInfo" json:"finalEvent,omitempty"`
-	Info       fftypes.JSONObject `ffstruct:"FireFlyContractInfo" json:"info,omitempty"`
+	Index        int              `ffstruct:"FireFlyContractInfo" json:"index"`
+	FinalEvent   string           `ffstruct:"FireFlyContractInfo" json:"finalEvent,omitempty"`
+	Location     *fftypes.JSONAny `ffstruct:"FireFlyContractInfo" json:"location,omitempty"`
+	FirstEvent   string           `ffstruct:"FireFlyContractInfo" json:"firstEvent,omitempty"`
+	Subscription string           `ffstruct:"FireFlyContractInfo" json:"subscription,omitempty"`
 }
 
 // NetworkActionType is a type of action to perform
diff --git a/pkg/dataexchange/plugin.go b/pkg/dataexchange/plugin.go
index 523a2d7b6..7a5746752 100644
--- a/pkg/dataexchange/plugin.go
+++ b/pkg/dataexchange/plugin.go
@@ -108,7 +108,6 @@ type DXEventType int
 
 // DXEvent is a single interface that can be passed to all events
 type DXEvent interface {
-	EventID() string
 	NamespacedID() string
 	Ack()
 	AckWithManifest(manifest string)


Test patch:
diff --git a/internal/batchpin/batchpin_test.go b/internal/batchpin/batchpin_test.go
index 73af2532d..8e095694d 100644
--- a/internal/batchpin/batchpin_test.go
+++ b/internal/batchpin/batchpin_test.go
@@ -24,10 +24,10 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/config"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/internal/coreconfig"
-	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
 	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/stretchr/testify/assert"
@@ -41,7 +41,7 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt
 
 	mdi := &databasemocks.Plugin{}
 	mim := &identitymanagermocks.Manager{}
-	mbi := &blockchainmocks.Plugin{}
+	mmp := &multipartymocks.Manager{}
 	mmi := &metricsmocks.Manager{}
 	mom := &operationmocks.Manager{}
 	mmi.On("IsMetricsEnabled").Return(enableMetrics)
@@ -49,8 +49,8 @@ func newTestBatchPinSubmitter(t *testing.T, enableMetrics bool) *batchPinSubmitt
 	if enableMetrics {
 		mmi.On("CountBatchPin").Return()
 	}
-	mbi.On("Name").Return("ut").Maybe()
-	bps, err := NewBatchPinSubmitter(context.Background(), "ns1", mdi, mim, mbi, mmi, mom)
+	mmp.On("Name").Return("ut").Maybe()
+	bps, err := NewBatchPinSubmitter(context.Background(), "ns1", mdi, mim, mmp, mmi, mom)
 	assert.NoError(t, err)
 	return bps.(*batchPinSubmitter)
 }
diff --git a/internal/batchpin/operations_test.go b/internal/batchpin/operations_test.go
index 9104391eb..5ba65ff89 100644
--- a/internal/batchpin/operations_test.go
+++ b/internal/batchpin/operations_test.go
@@ -21,8 +21,8 @@ import (
 	"testing"
 
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
-	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -50,10 +50,10 @@ func TestPrepareAndRunBatchPin(t *testing.T) {
 	}
 	addBatchPinInputs(op, batch.ID, contexts, "payload1")
 
-	mbi := bp.blockchain.(*blockchainmocks.Plugin)
+	mmp := bp.multiparty.(*multipartymocks.Manager)
 	mdi := bp.database.(*databasemocks.Plugin)
 	mdi.On("GetBatchByID", context.Background(), "ns1", batch.ID).Return(batch, nil)
-	mbi.On("SubmitBatchPin", context.Background(), "ns1:"+op.ID.String(), "0x123", mock.Anything).Return(nil)
+	mmp.On("SubmitBatchPin", context.Background(), "ns1:"+op.ID.String(), "0x123", mock.Anything).Return(nil)
 
 	po, err := bp.PrepareOperation(context.Background(), op)
 	assert.NoError(t, err)
diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 7622918bf..9ae9304a5 100644
--- a/internal/blockchain/ethereum/ethereum_test.go
+++ b/internal/blockchain/ethereum/ethereum_test.go
@@ -92,7 +92,6 @@ func newTestEthereum() (*Ethereum, func()) {
 		wsconn:      wsm,
 		metrics:     mm,
 	}
-	e.fireflyContract.address = "/instances/0x12345"
 	return e, func() {
 		cancel()
 		if e.closed != nil {
@@ -166,16 +165,6 @@ func TestInitAndStartWithFFTM(t *testing.T) {
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
 	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL),
 		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL),
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL),
-		func(req *http.Request) (*http.Response, error) {
-			var body map[string]interface{}
-			json.NewDecoder(req.Body).Decode(&body)
-			assert.Equal(t, "es12345", body["stream"])
-			return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req)
-		})
-	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/", httpURL), mockNetworkVersion(t, 1))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL)
@@ -191,12 +180,10 @@ func TestInitAndStartWithFFTM(t *testing.T) {
 	assert.Equal(t, "ethereum", e.Name())
 	assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType())
 
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
 	assert.NoError(t, err)
 
-	assert.Equal(t, 5, httpmock.GetTotalCallCount())
+	assert.Equal(t, 2, httpmock.GetTotalCallCount())
 	assert.Equal(t, "es12345", e.streamID)
-	assert.Equal(t, "sub12345", e.fireflyContract.subscription)
 	assert.NotNil(t, e.Capabilities())
 
 	err = e.Start()
@@ -232,68 +219,6 @@ func TestWSInitFail(t *testing.T) {
 
 }
 
-func TestInitMissingInstance(t *testing.T) {
-
-	e, cancel := newTestEthereum()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10138.*instance", err)
-
-}
-
-func TestInitAllExistingStreams(t *testing.T) {
-
-	e, cancel := newTestEthereum()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{
-			{ID: "sub12345", Stream: "es12345", Name: "BatchPin_3078373163373635" /* this is the subname for our combo of instance path and BatchPin */},
-		}))
-	httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345",
-		httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.NoError(t, err)
-
-	assert.Equal(t, 4, httpmock.GetTotalCallCount())
-	assert.Equal(t, "es12345", e.streamID)
-	assert.Equal(t, "sub12345", e.fireflyContract.subscription)
-
-}
-
 func TestInitOldInstancePathContracts(t *testing.T) {
 	e, cancel := newTestEthereum()
 	defer cancel()
@@ -336,10 +261,7 @@ func TestInitOldInstancePathContracts(t *testing.T) {
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
 	assert.NoError(t, err)
-
-	assert.Equal(t, e.fireflyContract.address, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
 }
 
 func TestInitOldInstancePathInstances(t *testing.T) {
@@ -374,48 +296,7 @@ func TestInitOldInstancePathInstances(t *testing.T) {
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.NoError(t, err)
-
-	assert.Equal(t, e.fireflyContract.address, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
-}
-
-func TestInitOldInstancePathError(t *testing.T) {
-	e, cancel := newTestEthereum()
-	defer cancel()
-	resetConf(e)
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		func(req *http.Request) (*http.Response, error) {
-			var body map[string]interface{}
-			json.NewDecoder(req.Body).Decode(&body)
-			assert.Equal(t, "es12345", body["stream"])
-			return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req)
-		})
-	httpmock.RegisterResponder("GET", "http://localhost:12345/contracts/firefly",
-		httpmock.NewJsonResponderOrPanic(500, "pop"),
-	)
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10111.*pop", err)
 }
 
 func TestInitNewConfig(t *testing.T) {
@@ -431,11 +312,6 @@ func TestInitNewConfig(t *testing.T) {
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
 	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
 		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
@@ -444,38 +320,8 @@ func TestInitNewConfig(t *testing.T) {
 	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
+	assert.Equal(t, 2, httpmock.GetTotalCallCount())
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.NoError(t, err)
-
-	assert.Equal(t, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", e.fireflyContract.address)
-	assert.Equal(t, 2, e.fireflyContract.networkVersion)
-}
-
-func TestInitNewConfigError(t *testing.T) {
-	e, cancel := newTestEthereum()
-	defer cancel()
-	resetConf(e)
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10138", err)
 }
 
 func TestInitNewConfigBadIndex(t *testing.T) {
@@ -500,10 +346,6 @@ func TestInitNewConfigBadIndex(t *testing.T) {
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{
-		Active: core.FireFlyContractInfo{Index: 1},
-	})
-	assert.Regexp(t, "FF10396", err)
 }
 
 func TestInitNetworkVersionNotFound(t *testing.T) {
@@ -534,49 +376,36 @@ func TestInitNetworkVersionNotFound(t *testing.T) {
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
 	assert.NoError(t, err)
-
-	assert.Equal(t, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", e.fireflyContract.address)
-	assert.Equal(t, 1, e.fireflyContract.networkVersion)
 }
 
-func TestInitNetworkVersionError(t *testing.T) {
+func TestStreamQueryError(t *testing.T) {
 	e, cancel := newTestEthereum()
 	defer cancel()
-	resetConf(e)
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/",
-		httpmock.NewJsonResponderOrPanic(500, ethError{Error: "Unknown"}))
+		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
 	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10111", err)
+	assert.Regexp(t, "FF10111.*pop", err)
+
 }
 
-func TestInitNetworkVersionBadResponse(t *testing.T) {
+func TestStreamCreateError(t *testing.T) {
+
 	e, cancel := newTestEthereum()
 	defer cancel()
-	resetConf(e)
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
@@ -585,135 +414,88 @@ func TestInitNetworkVersionBadResponse(t *testing.T) {
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
 	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/",
-		httpmock.NewJsonResponderOrPanic(200, ""))
+		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
 	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "json: cannot unmarshal", err)
+	assert.Regexp(t, "FF10111.*pop", err)
+
 }
 
-func TestInitTerminateContract(t *testing.T) {
-	e, _ := newTestEthereum()
+func TestStreamUpdateError(t *testing.T) {
 
-	contracts := &core.FireFlyContracts{}
-	event := &blockchain.Event{
-		ProtocolID: "000000000011/000000/000050",
-		Info: fftypes.JSONObject{
-			"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
-		},
-	}
+	e, cancel := newTestEthereum()
+	defer cancel()
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1))
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
+	httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345",
+		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
 	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x1C197604587F046FD40684A8f21f4609FB811A7b")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".1."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, contracts)
-	assert.NoError(t, err)
-
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-2"}))
-
-	err = e.TerminateContract(e.ctx, contracts, event)
-	assert.NoError(t, err)
-
-	assert.Equal(t, 1, contracts.Active.Index)
-	assert.Equal(t, fftypes.JSONObject{
-		"address":      "0x71c7656ec7ab88b098defb751b7401b5f6d8976f",
-		"fromBlock":    "oldest",
-		"subscription": "sb-2",
-	}, contracts.Active.Info)
-	assert.Len(t, contracts.Terminated, 1)
-	assert.Equal(t, 0, contracts.Terminated[0].Index)
-	assert.Equal(t, fftypes.JSONObject{
-		"address":      "0x1c197604587f046fd40684a8f21f4609fb811a7b",
-		"fromBlock":    "oldest",
-		"subscription": "sb-1",
-	}, contracts.Terminated[0].Info)
-	assert.Equal(t, event.ProtocolID, contracts.Terminated[0].FinalEvent)
+	assert.Regexp(t, "FF10111.*pop", err)
 }
 
-func TestInitTerminateContractIgnore(t *testing.T) {
-	e, _ := newTestEthereum()
-
-	contracts := &core.FireFlyContracts{}
-	event := &blockchain.Event{
-		ProtocolID: "000000000011/000000/000050",
-		Info: fftypes.JSONObject{
-			"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
-		},
-	}
+func TestInitAllExistingStreams(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
 	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
+		httpmock.NewJsonResponderOrPanic(200, []subscription{
+			{ID: "sub12345", Stream: "es12345", Name: "BatchPin_3078373143373635" /* this is the subname for our combo of instance path and BatchPin */},
+		}))
+	httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345",
+		httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1))
 	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
 		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
 	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
+	}.String())
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, contracts)
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
 	assert.NoError(t, err)
 
-	err = e.TerminateContract(e.ctx, contracts, event)
-	assert.NoError(t, err)
+	assert.Equal(t, 3, httpmock.GetTotalCallCount())
+	assert.Equal(t, "es12345", e.streamID)
 }
 
-func TestInitTerminateContractBadEvent(t *testing.T) {
-	e, _ := newTestEthereum()
+func TestSubQueryError(t *testing.T) {
 
-	contracts := &core.FireFlyContracts{}
-	event := &blockchain.Event{
-		ProtocolID: "000000000011/000000/000050",
-		Info: fftypes.JSONObject{
-			"address": "bad",
-		},
-	}
+	e, cancel := newTestEthereum()
+	defer cancel()
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
@@ -724,27 +506,20 @@ func TestInitTerminateContractBadEvent(t *testing.T) {
 	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
 		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
 	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1))
+		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
 	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
 	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, contracts)
-	assert.NoError(t, err)
-
-	err = e.TerminateContract(e.ctx, contracts, event)
-	assert.Regexp(t, "FF10141", err)
 }
 
-func TestStreamQueryError(t *testing.T) {
+func TestSubQueryCreateError(t *testing.T) {
 
 	e, cancel := newTestEthereum()
 	defer cancel()
@@ -754,6 +529,12 @@ func TestStreamQueryError(t *testing.T) {
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
 		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
@@ -764,126 +545,49 @@ func TestStreamQueryError(t *testing.T) {
 	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
 
 	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.Regexp(t, "FF10111.*pop", err)
-
+	assert.NoError(t, err)
 }
 
-func TestStreamCreateError(t *testing.T) {
-
+func TestSubmitBatchPinOK(t *testing.T) {
 	e, cancel := newTestEthereum()
 	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
+	httpmock.ActivateNonDefault(e.client.GetClient())
 	defer httpmock.DeactivateAndReset()
 
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewStringResponder(500, `pop`))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.Regexp(t, "FF10111.*pop", err)
-
-}
-
-func TestStreamUpdateError(t *testing.T) {
-
-	e, cancel := newTestEthereum()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
-	httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345",
-		httpmock.NewStringResponder(500, `pop`))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.Regexp(t, "FF10111.*pop", err)
-
-}
-
-func TestSubQueryError(t *testing.T) {
-
-	e, cancel := newTestEthereum()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewStringResponder(500, `pop`))
-
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10111.*pop", err)
-
-}
-
-func TestSubQueryCreateError(t *testing.T) {
-
-	e, cancel := newTestEthereum()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
+	addr := ethHexFormatB32(fftypes.NewRandB32())
+	batch := &blockchain.BatchPin{
+		TransactionID:   fftypes.MustParseUUID("9ffc50ff-6bfe-4502-adc7-93aea54cc059"),
+		BatchID:         fftypes.MustParseUUID("c5df767c-fe44-4e03-8eb5-1c5523097db5"),
+		BatchHash:       fftypes.NewRandB32(),
+		BatchPayloadRef: "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD",
+		Contexts: []*fftypes.Bytes32{
+			fftypes.NewRandB32(),
+			fftypes.NewRandB32(),
+		},
+	}
 
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewStringResponder(500, `pop`))
+	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			params := body["params"].([]interface{})
+			headers := body["headers"].(map[string]interface{})
+			assert.Equal(t, "SendTransaction", headers["type"])
+			assert.Equal(t, "0x9ffc50ff6bfe4502adc793aea54cc059c5df767cfe444e038eb51c5523097db5", params[1])
+			assert.Equal(t, ethHexFormatB32(batch.BatchHash), params[2])
+			assert.Equal(t, "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", params[3])
+			return httpmock.NewJsonResponderOrPanic(200, "")(req)
+		})
 
-	resetConf(e)
-	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
-	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
-	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	err := e.SubmitBatchPin(context.Background(), "", addr, batch, location)
 
-	err := e.Init(e.ctx, utConfig, e.metrics)
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10111.*pop", err)
-
 }
 
-func TestSubmitBatchPinOK(t *testing.T) {
-
+func TestSubmitBatchPinBadLocation(t *testing.T) {
 	e, cancel := newTestEthereum()
 	defer cancel()
 	httpmock.ActivateNonDefault(e.client.GetClient())
@@ -914,10 +618,12 @@ func TestSubmitBatchPinOK(t *testing.T) {
 			return httpmock.NewJsonResponderOrPanic(200, "")(req)
 		})
 
-	err := e.SubmitBatchPin(context.Background(), "", addr, batch)
-
-	assert.NoError(t, err)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "bad",
+	}.String())
+	err := e.SubmitBatchPin(context.Background(), "", addr, batch, location)
 
+	assert.Regexp(t, "FF10310", err)
 }
 
 func TestSubmitBatchEmptyPayloadRef(t *testing.T) {
@@ -951,7 +657,11 @@ func TestSubmitBatchEmptyPayloadRef(t *testing.T) {
 			return httpmock.NewJsonResponderOrPanic(200, "")(req)
 		})
 
-	err := e.SubmitBatchPin(context.Background(), "", addr, batch)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	err := e.SubmitBatchPin(context.Background(), "", addr, batch, location)
 
 	assert.NoError(t, err)
 
@@ -975,11 +685,14 @@ func TestSubmitBatchPinFail(t *testing.T) {
 			fftypes.NewRandB32(),
 		},
 	}
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
 
 	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
 		httpmock.NewStringResponder(500, "pop"))
 
-	err := e.SubmitBatchPin(context.Background(), "", addr, batch)
+	err := e.SubmitBatchPin(context.Background(), "", addr, batch, location)
 
 	assert.Regexp(t, "FF10111.*pop", err)
 
@@ -1004,15 +717,17 @@ func TestSubmitBatchPinError(t *testing.T) {
 		},
 	}
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
 	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
 		httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{
 			"error": "Unknown error",
 		}))
-
-	err := e.SubmitBatchPin(context.Background(), "", addr, batch)
+	err := e.SubmitBatchPin(context.Background(), "", addr, batch, location)
 
 	assert.Regexp(t, "FF10111.*Unknown error", err)
-
 }
 
 func TestVerifyEthAddress(t *testing.T) {
@@ -1098,8 +813,9 @@ func TestHandleMessageBatchPinOK(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-	e.fireflyContract.networkVersion = 1
+
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
@@ -1181,7 +897,8 @@ func TestHandleMessageBatchPinMissingAuthor(t *testing.T) {
 ]`)
 
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal(data.Bytes(), &events)
@@ -1221,8 +938,9 @@ func TestHandleMessageEmptyPayloadRef(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-	e.fireflyContract.networkVersion = 1
+
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
@@ -1283,9 +1001,8 @@ func TestHandleMessageBatchPinExit(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-	e.fireflyContract.networkVersion = 1
-
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	em.On("BatchPinComplete", mock.Anything, expectedSigningKeyRef, mock.Anything).Return(fmt.Errorf("pop"))
 
 	var events []interface{}
@@ -1299,7 +1016,8 @@ func TestHandleMessageBatchPinExit(t *testing.T) {
 
 func TestHandleMessageBatchPinEmpty(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal([]byte(`
@@ -1316,7 +1034,8 @@ func TestHandleMessageBatchPinEmpty(t *testing.T) {
 
 func TestHandleMessageBatchMissingData(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal([]byte(`
@@ -1334,7 +1053,8 @@ func TestHandleMessageBatchMissingData(t *testing.T) {
 
 func TestHandleMessageBatchPinBadTransactionID(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	data := fftypes.JSONAnyPtr(`[{
 		"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
 		"subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5",
@@ -1364,7 +1084,8 @@ func TestHandleMessageBatchPinBadTransactionID(t *testing.T) {
 
 func TestHandleMessageBatchPinBadIDentity(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	data := fftypes.JSONAnyPtr(`[{
 		"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
 		"subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5",
@@ -1394,7 +1115,8 @@ func TestHandleMessageBatchPinBadIDentity(t *testing.T) {
 
 func TestHandleMessageBatchPinBadBatchHash(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	data := fftypes.JSONAnyPtr(`[{
 		"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
 		"subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5",
@@ -1424,7 +1146,8 @@ func TestHandleMessageBatchPinBadBatchHash(t *testing.T) {
 
 func TestHandleMessageBatchPinBadPin(t *testing.T) {
 	e := &Ethereum{}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	data := fftypes.JSONAnyPtr(`[{
 		"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
 		"subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5",
@@ -1823,8 +1546,8 @@ func TestHandleMessageContractEvent(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	em.On("BlockchainEvent", mock.MatchedBy(func(e *blockchain.EventWithSubscription) bool {
 		assert.Equal(t, "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", e.BlockchainTXID)
 		assert.Equal(t, "000000038011/000000/000050", e.Event.ProtocolID)
@@ -1885,8 +1608,8 @@ func TestHandleMessageContractEventError(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	em.On("BlockchainEvent", mock.Anything).Return(fmt.Errorf("pop"))
 
 	var events []interface{}
@@ -2282,6 +2005,30 @@ func TestGetContractAddressBadJSON(t *testing.T) {
 	assert.Regexp(t, "invalid character 'n' looking for beginning of object key string", err)
 }
 
+func TestGetContractAddressBadResponse(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/contracts/firefly",
+		httpmock.NewJsonResponderOrPanic(500, ethError{Error: "FFEC100148"}))
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x12345")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+
+	e.client = ffresty.New(e.ctx, utEthconnectConf)
+
+	_, err := e.getContractAddress(context.Background(), "/contracts/firefly")
+
+	assert.Regexp(t, "FF10111", err)
+}
+
 func TestFFIMethodToABI(t *testing.T) {
 	e, _ := newTestEthereum()
 
@@ -3046,10 +2793,39 @@ func TestSubmitNetworkAction(t *testing.T) {
 			return httpmock.NewJsonResponderOrPanic(200, "")(req)
 		})
 
-	err := e.SubmitNetworkAction(context.Background(), "ns1:"+fftypes.NewUUID().String(), "0x123", core.NetworkActionTerminate)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	err := e.SubmitNetworkAction(context.Background(), "ns1:"+fftypes.NewUUID().String(), "0x123", core.NetworkActionTerminate, location)
 	assert.NoError(t, err)
 }
 
+func TestSubmitNetworkActionBadLocation(t *testing.T) {
+	e, _ := newTestEthereum()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			params := body["params"].([]interface{})
+			headers := body["headers"].(map[string]interface{})
+			assert.Equal(t, "SendTransaction", headers["type"])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[1])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[2])
+			assert.Equal(t, "", params[3])
+			return httpmock.NewJsonResponderOrPanic(200, "")(req)
+		})
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "pop",
+	}.String())
+
+	err := e.SubmitNetworkAction(context.Background(), "ns1:"+fftypes.NewUUID().String(), "0x123", core.NetworkActionTerminate, location)
+	assert.Regexp(t, "FF10310", err)
+}
+
 func TestHandleNetworkAction(t *testing.T) {
 	data := fftypes.JSONAnyPtr(`
 [
@@ -3077,7 +2853,8 @@ func TestHandleNetworkAction(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
@@ -3123,7 +2900,8 @@ func TestHandleNetworkActionFail(t *testing.T) {
 	e := &Ethereum{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
@@ -3249,8 +3027,324 @@ func TestConvertABIEventFFIEvent(t *testing.T) {
 	assert.JSONEq(t, string(expectedABIEventJSON), string(actualABIEventJSON))
 }
 
-func TestNetworkVersion(t *testing.T) {
+func TestGetNetworkVersion(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			headers := body["headers"].(map[string]interface{})
+			assert.Equal(t, "Query", headers["type"])
+			return httpmock.NewJsonResponderOrPanic(200, queryOutput{Output: "1"})(req)
+		})
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, version)
+}
+
+func TestGetNetworkVersionMethodNotFound(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	httpmock.RegisterResponder("POST", "http://localhost:12345/",
+		httpmock.NewJsonResponderOrPanic(500, ethError{Error: "FFEC100148"}))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, version)
+}
+
+func TestGetNetworkVersionQueryFail(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	httpmock.RegisterResponder("POST", "http://localhost:12345/",
+		httpmock.NewJsonResponderOrPanic(500, ethError{Error: "pop"}))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "pop", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestGetNetworkVersionBadLocation(t *testing.T) {
+	e, _ := newTestEthereum()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "pop",
+	}.String())
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "FF10310", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestGetNetworkVersionUnmarshalFail(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			headers := body["headers"].(map[string]interface{})
+			assert.Equal(t, "Query", headers["type"])
+			return httpmock.NewJsonResponderOrPanic(200, "")(req)
+		})
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "cannot unmarshal", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestConvertDeprecatedContractConfig(t *testing.T) {
+	e, _ := newTestEthereum()
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71c7656ec7ab88b098defb751b7401b5f6d8976f")
+
+	locationBytes, fromBlock, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Equal(t, "0", fromBlock)
+	assert.NoError(t, err)
+
+	location, err := parseContractLocation(e.ctx, locationBytes)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", location.Address)
+}
+
+func TestConvertDeprecatedContractConfigNoInstance(t *testing.T) {
+	e, _ := newTestEthereum()
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+
+	_, _, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Regexp(t, "F10138", err)
+}
+
+func TestConvertDeprecatedContractConfigContractURL(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/contracts/firefly",
+		httpmock.NewJsonResponderOrPanic(200, map[string]string{
+			"created":      "2022-02-08T22:10:10Z",
+			"address":      "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
+			"path":         "/contracts/firefly",
+			"abi":          "fc49dec3-0660-4dc7-61af-65af4c3ac456",
+			"openapi":      "/contracts/firefly?swagger",
+			"registeredAs": "firefly",
+		}),
+	)
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+
+	e.client = ffresty.New(e.ctx, utEthconnectConf)
+
+	locationBytes, fromBlock, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.NoError(t, err)
+	assert.Equal(t, "0", fromBlock)
+
+	location, err := parseContractLocation(e.ctx, locationBytes)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", location.Address)
+}
+
+func TestConvertDeprecatedContractConfigContractURLBadQuery(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/contracts/firefly",
+		httpmock.NewJsonResponderOrPanic(500, ethError{Error: "FFEC100148"}))
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+
+	e.client = ffresty.New(e.ctx, utEthconnectConf)
+
+	_, _, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Regexp(t, "FFEC100148", err)
+}
+
+func TestConvertDeprecatedContractConfigBadAddress(t *testing.T) {
+	e, _ := newTestEthereum()
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/bad")
+
+	_, _, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Regexp(t, "FF10141", err)
+}
+
+func TestAddSubBadLocation(t *testing.T) {
 	e, _ := newTestEthereum()
-	e.fireflyContract.networkVersion = 2
-	assert.Equal(t, 2, e.NetworkVersion())
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "bad",
+	}.String())
+
+	_, err := e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
+	assert.Regexp(t, "FF10310", err)
+}
+
+func TestAddAndRemoveFireflySubscription(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	resetConf(e)
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(200, subscription{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2))
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
+
+	err := e.Init(e.ctx, utConfig, e.metrics)
+	assert.NoError(t, err)
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	subID, err := e.AddFireflySubscription(e.ctx, "ns1", location, "newest")
+	assert.NoError(t, err)
+
+	err = e.RemoveFireflySubscription(e.ctx, subID)
+	assert.NoError(t, err)
+}
+
+func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	resetConf(e)
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
+		httpmock.NewStringResponder(500, `pop`))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(200, subscription{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2))
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
+
+	err := e.Init(e.ctx, utConfig, e.metrics)
+	assert.NoError(t, err)
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
+	assert.Regexp(t, "FF10111", err)
+}
+
+func TestAddFireflySubscriptionCreateError(t *testing.T) {
+	e, cancel := newTestEthereum()
+	defer cancel()
+	resetConf(e)
+
+	mockedClient := &http.Client{}
+	httpmock.ActivateNonDefault(mockedClient)
+	defer httpmock.DeactivateAndReset()
+
+	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
+		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
+		httpmock.NewJsonResponderOrPanic(500, `pop`))
+	httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2))
+
+	resetConf(e)
+	utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utEthconnectConf.Set(EthconnectConfigTopic, "topic1")
+	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
+
+	err := e.Init(e.ctx, utConfig, e.metrics)
+	assert.NoError(t, err)
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
+	assert.Regexp(t, "FF10111", err)
+}
+
+func TestRemoveInvalidSubscription(t *testing.T) {
+	e, _ := newTestEthereum()
+
+	err := e.RemoveFireflySubscription(e.ctx, "bad")
+	assert.Regexp(t, "FF10412", err)
 }
diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go
index 8808cad7f..8958e2f0b 100644
--- a/internal/blockchain/fabric/fabric_test.go
+++ b/internal/blockchain/fabric/fabric_test.go
@@ -63,7 +63,6 @@ func newTestFabric() (*Fabric, func()) {
 		prefixLong:     defaultPrefixLong,
 		wsconn:         wsm,
 	}
-	e.fireflyContract.chaincode = "firefly"
 	return e, func() {
 		cancel()
 		if e.closed != nil {
@@ -153,17 +152,6 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) {
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
 	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL),
 		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL),
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL),
-		func(req *http.Request) (*http.Response, error) {
-			var body map[string]interface{}
-			json.NewDecoder(req.Body).Decode(&body)
-			assert.Equal(t, "es12345", body["stream"])
-			assert.Equal(t, "0", body["fromBlock"])
-			return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req)
-		})
-	httpmock.RegisterResponder("POST", fmt.Sprintf("%s/query", httpURL), mockNetworkVersion(t, 1))
 
 	resetConf(e)
 	utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL)
@@ -178,14 +166,12 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) {
 	assert.Equal(t, "fabric", e.Name())
 	assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType())
 
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
 	assert.NoError(t, err)
 	err = e.Start()
 	assert.NoError(t, err)
 
-	assert.Equal(t, 5, httpmock.GetTotalCallCount())
+	assert.Equal(t, 2, httpmock.GetTotalCallCount())
 	assert.Equal(t, "es12345", e.streamID)
-	assert.Equal(t, "sub12345", e.fireflyContract.subscription)
 	assert.NotNil(t, e.Capabilities())
 
 	startupMessage := <-toServer
@@ -219,34 +205,7 @@ func TestWSInitFail(t *testing.T) {
 
 }
 
-func TestInitMissingInstance(t *testing.T) {
-
-	e, cancel := newTestFabric()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-
-	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10138.*chaincode", err)
-
-}
-
 func TestInitAllExistingStreams(t *testing.T) {
-
 	e, cancel := newTestFabric()
 	defer cancel()
 
@@ -258,9 +217,8 @@ func TestInitAllExistingStreams(t *testing.T) {
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
 	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
 		httpmock.NewJsonResponderOrPanic(200, []subscription{
-			{ID: "sub12345", Stream: "es12345", Name: "BatchPin"},
+			{ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin"},
 		}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), mockNetworkVersion(t, 1))
 
 	resetConf(e)
 	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
@@ -269,320 +227,107 @@ func TestInitAllExistingStreams(t *testing.T) {
 	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
 
-	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.NoError(t, err)
-
-	assert.Equal(t, 3, httpmock.GetTotalCallCount())
-	assert.Equal(t, "es12345", e.streamID)
-	assert.Equal(t, "sub12345", e.fireflyContract.subscription)
-
-}
-
-func TestInitNewConfig(t *testing.T) {
-
-	e, cancel := newTestFabric()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{
-			{ID: "sub12345", Stream: "es12345", Name: "BatchPin"},
-		}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"),
-		mockNetworkVersion(t, 1))
-
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
 
 	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
 	assert.NoError(t, err)
 
-	assert.Equal(t, 3, httpmock.GetTotalCallCount())
+	assert.Equal(t, 2, httpmock.GetTotalCallCount())
 	assert.Equal(t, "es12345", e.streamID)
-	assert.Equal(t, "sub12345", e.fireflyContract.subscription)
-
 }
 
-func TestInitNewConfigError(t *testing.T) {
-
+func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) {
 	e, cancel := newTestFabric()
 	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
-
 	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "")
-
-	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10138", err)
-
-}
-
-func TestInitNewConfigBadIndex(t *testing.T) {
-
-	e, cancel := newTestFabric()
-	defer cancel()
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
-
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "")
-
-	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{
-		Active: core.FireFlyContractInfo{Index: 1},
-	})
-	assert.Regexp(t, "FF10396", err)
-
-}
-
-func TestInitNetworkVersionNotFound(t *testing.T) {
-	e, cancel := newTestFabric()
-	defer cancel()
-	resetConf(e)
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
 	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
-		httpmock.NewJsonResponderOrPanic(500, fabError{Error: "Function NetworkVersion not found"}))
-
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.NoError(t, err)
-
-	assert.Equal(t, "firefly", e.fireflyContract.chaincode)
-	assert.Equal(t, 1, e.fireflyContract.networkVersion)
-}
-
-func TestInitNetworkVersionError(t *testing.T) {
-	e, cancel := newTestFabric()
-	defer cancel()
-	resetConf(e)
+		httpmock.NewJsonResponderOrPanic(500, "pop"))
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
 	defer httpmock.DeactivateAndReset()
 
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
-		httpmock.NewJsonResponderOrPanic(500, fabError{Error: "Unknown"}))
-
-	resetConf(e)
 	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
 	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly")
 	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
 
-	err := e.Init(e.ctx, utConfig, e.metrics)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "FF10284", err)
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "newest")
+	assert.Regexp(t, "pop", err)
 }
 
-func TestInitNetworkVersionBadResponse(t *testing.T) {
+func TestAddAndRemoveFireflySubscriptionDeprecatedSubName(t *testing.T) {
 	e, cancel := newTestFabric()
 	defer cancel()
-	resetConf(e)
 
 	mockedClient := &http.Client{}
 	httpmock.ActivateNonDefault(mockedClient)
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
 	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
-		httpmock.NewJsonResponderOrPanic(200, ""))
+		httpmock.NewJsonResponderOrPanic(200, []subscription{
+			{ID: "sub12345", Stream: "es12345", Name: "BatchPin"},
+		}))
 
 	resetConf(e)
 	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
 	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
+	utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly")
 	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
-
-	err := e.Init(e.ctx, utConfig, e.metrics)
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
-	assert.Regexp(t, "json: cannot unmarshal", err)
-}
-
-func TestInitTerminateContract(t *testing.T) {
-	e, _ := newTestFabric()
-
-	contracts := &core.FireFlyContracts{}
-	event := &blockchain.Event{
-		ProtocolID: "000000000011/000000/000050",
-		Info: fftypes.JSONObject{
-			"chaincodeId": "firefly",
-		},
-	}
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"),
-		mockNetworkVersion(t, 1))
 
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".1."+FireFlyContractChaincode, "firefly2")
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
 
 	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, contracts)
+	subID, err := e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
 	assert.NoError(t, err)
 
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-2"}))
+	assert.Equal(t, 2, httpmock.GetTotalCallCount())
+	assert.Equal(t, "es12345", e.streamID)
 
-	err = e.TerminateContract(e.ctx, contracts, event)
+	err = e.RemoveFireflySubscription(e.ctx, subID)
 	assert.NoError(t, err)
-
-	assert.Equal(t, 1, contracts.Active.Index)
-	assert.Equal(t, fftypes.JSONObject{
-		"chaincode":    "firefly2",
-		"fromBlock":    "oldest",
-		"subscription": "sb-2",
-	}, contracts.Active.Info)
-	assert.Len(t, contracts.Terminated, 1)
-	assert.Equal(t, 0, contracts.Terminated[0].Index)
-	assert.Equal(t, fftypes.JSONObject{
-		"chaincode":    "firefly",
-		"fromBlock":    "oldest",
-		"subscription": "sb-1",
-	}, contracts.Terminated[0].Info)
-	assert.Equal(t, event.ProtocolID, contracts.Terminated[0].FinalEvent)
 }
 
-func TestInitTerminateContractIgnore(t *testing.T) {
+func TestRemoveUnknownFireflySubscription(t *testing.T) {
 	e, _ := newTestFabric()
 
-	contracts := &core.FireFlyContracts{}
-	event := &blockchain.Event{
-		ProtocolID: "000000000011/000000/000050",
-		Info: fftypes.JSONObject{
-			"chaincodeId": "firefly2",
-		},
-	}
-
-	mockedClient := &http.Client{}
-	httpmock.ActivateNonDefault(mockedClient)
-	defer httpmock.DeactivateAndReset()
-
-	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, []subscription{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
-		httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"}))
-	httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"),
-		mockNetworkVersion(t, 1))
-
-	resetConf(e)
-	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
-	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
-
-	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, contracts)
-	assert.NoError(t, err)
-
-	err = e.TerminateContract(e.ctx, contracts, event)
-	assert.NoError(t, err)
-
-	assert.Equal(t, 0, contracts.Active.Index)
-	assert.Equal(t, fftypes.JSONObject{
-		"chaincode":    "firefly",
-		"fromBlock":    "oldest",
-		"subscription": "sb-1",
-	}, contracts.Active.Info)
-	assert.Len(t, contracts.Terminated, 0)
+	err := e.RemoveFireflySubscription(e.ctx, "does not exist")
+	assert.Regexp(t, "FF10412", err)
 }
 
-func TestStreamQueryError(t *testing.T) {
+func TestAddFFSubscriptionBadLocation(t *testing.T) {
+	e, _ := newTestFabric()
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "bad",
+	}.String())
+	_, err := e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
+	assert.Regexp(t, "F10310", err)
+}
 
+func TestInitNewConfig(t *testing.T) {
 	e, cancel := newTestFabric()
 	defer cancel()
 
@@ -591,22 +336,23 @@ func TestStreamQueryError(t *testing.T) {
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewStringResponder(500, `pop`))
+		httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}}))
 
 	resetConf(e)
 	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
-	utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false)
 	utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient)
-	utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly")
 	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
+	utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly")
 
 	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.Regexp(t, "FF10284.*pop", err)
+	assert.NoError(t, err)
 
+	assert.Equal(t, 1, httpmock.GetTotalCallCount())
+	assert.Equal(t, "es12345", e.streamID)
 }
 
-func TestStreamCreateError(t *testing.T) {
+func TestStreamQueryError(t *testing.T) {
 
 	e, cancel := newTestFabric()
 	defer cancel()
@@ -616,8 +362,6 @@ func TestStreamCreateError(t *testing.T) {
 	defer httpmock.DeactivateAndReset()
 
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
-	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
 		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
@@ -633,7 +377,7 @@ func TestStreamCreateError(t *testing.T) {
 
 }
 
-func TestSubQueryError(t *testing.T) {
+func TestStreamCreateError(t *testing.T) {
 
 	e, cancel := newTestFabric()
 	defer cancel()
@@ -645,8 +389,6 @@ func TestSubQueryError(t *testing.T) {
 	httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams",
 		httpmock.NewJsonResponderOrPanic(200, []eventStream{}))
 	httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams",
-		httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"}))
-	httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
 		httpmock.NewStringResponder(500, `pop`))
 
 	resetConf(e)
@@ -658,8 +400,6 @@ func TestSubQueryError(t *testing.T) {
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
 
 	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
-	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
 	assert.Regexp(t, "FF10284.*pop", err)
 
 }
@@ -690,9 +430,14 @@ func TestSubQueryCreateError(t *testing.T) {
 	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
 	utFabconnectConf.Set(FabconnectConfigTopic, "topic1")
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
 	err := e.Init(e.ctx, utConfig, &metricsmocks.Manager{})
 	assert.NoError(t, err)
-	err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{})
+	_, err = e.AddFireflySubscription(e.ctx, "ns1", location, "oldest")
 	assert.Regexp(t, "FF10284.*pop", err)
 
 }
@@ -716,6 +461,11 @@ func TestSubmitBatchPinOK(t *testing.T) {
 		},
 	}
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
 	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
 		func(req *http.Request) (*http.Response, error) {
 			var body map[string]interface{}
@@ -727,12 +477,37 @@ func TestSubmitBatchPinOK(t *testing.T) {
 			return httpmock.NewJsonResponderOrPanic(200, "")(req)
 		})
 
-	err := e.SubmitBatchPin(context.Background(), "", signer, batch)
+	err := e.SubmitBatchPin(context.Background(), "", signer, batch, location)
 
 	assert.NoError(t, err)
 
 }
 
+func TestSubmitBatchPinBadLocation(t *testing.T) {
+
+	e, _ := newTestFabric()
+
+	signer := "signer001"
+	batch := &blockchain.BatchPin{
+		TransactionID:   fftypes.MustParseUUID("9ffc50ff-6bfe-4502-adc7-93aea54cc059"),
+		BatchID:         fftypes.MustParseUUID("c5df767c-fe44-4e03-8eb5-1c5523097db5"),
+		BatchHash:       fftypes.NewRandB32(),
+		BatchPayloadRef: "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD",
+		Contexts: []*fftypes.Bytes32{
+			fftypes.NewRandB32(),
+			fftypes.NewRandB32(),
+		},
+	}
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "simplestorage",
+	}.String())
+
+	err := e.SubmitBatchPin(context.Background(), "", signer, batch, location)
+
+	assert.Regexp(t, "FF10310", err)
+}
+
 func TestSubmitBatchEmptyPayloadRef(t *testing.T) {
 
 	e, cancel := newTestFabric()
@@ -751,6 +526,11 @@ func TestSubmitBatchEmptyPayloadRef(t *testing.T) {
 		},
 	}
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
 	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
 		func(req *http.Request) (*http.Response, error) {
 			var body map[string]interface{}
@@ -762,7 +542,7 @@ func TestSubmitBatchEmptyPayloadRef(t *testing.T) {
 			return httpmock.NewJsonResponderOrPanic(200, "")(req)
 		})
 
-	err := e.SubmitBatchPin(context.Background(), "", signer, batch)
+	err := e.SubmitBatchPin(context.Background(), "", signer, batch, location)
 
 	assert.NoError(t, err)
 
@@ -787,10 +567,15 @@ func TestSubmitBatchPinFail(t *testing.T) {
 		},
 	}
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
 	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
 		httpmock.NewStringResponder(500, "pop"))
 
-	err := e.SubmitBatchPin(context.Background(), "", signer, batch)
+	err := e.SubmitBatchPin(context.Background(), "", signer, batch, location)
 
 	assert.Regexp(t, "FF10284.*pop", err)
 
@@ -815,12 +600,17 @@ func TestSubmitBatchPinError(t *testing.T) {
 		},
 	}
 
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
 	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
 		httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{
 			"error": "Invalid",
 		}))
 
-	err := e.SubmitBatchPin(context.Background(), "", signer, batch)
+	err := e.SubmitBatchPin(context.Background(), "", signer, batch, location)
 
 	assert.Regexp(t, "FF10284.*Invalid", err)
 
@@ -945,7 +735,8 @@ func TestHandleMessageBatchPinOK(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeMSPIdentity,
@@ -992,7 +783,8 @@ func TestHandleMessageEmptyPayloadRef(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeMSPIdentity,
@@ -1039,7 +831,8 @@ func TestHandleMessageBatchPinExit(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeMSPIdentity,
@@ -1072,7 +865,8 @@ func TestHandleMessageBatchPinEmpty(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal(data, &events)
@@ -1098,7 +892,8 @@ func TestHandleMessageUnknownEventName(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal(data, &events)
@@ -1113,7 +908,9 @@ func TestHandleMessageBatchPinBadBatchHash(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
+
 	data := []byte(`[{
 		"chaincodeId": "firefly",
 		"blockNumber": 91,
@@ -1135,7 +932,8 @@ func TestHandleMessageBatchPinBadPin(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 	data := []byte(`[{
 		"chaincodeId": "firefly",
 		"blockNumber": 91,
@@ -1157,7 +955,9 @@ func TestHandleMessageBatchPinBadPayloadEncoding(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
+
 	data := []byte(`[{
 		"chaincodeId": "firefly",
 		"blockNumber": 91,
@@ -1179,7 +979,8 @@ func TestHandleMessageBatchPinBadPayloadUUIDs(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 	data := []byte(`[{
 		"chaincodeId": "firefly",
 		"blockNumber": 91,
@@ -1547,8 +1348,8 @@ func TestHandleMessageContractEvent(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
-
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 	em.On("BlockchainEvent", mock.MatchedBy(func(e *blockchain.EventWithSubscription) bool {
 		assert.Equal(t, "4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d", e.BlockchainTXID)
 		assert.Equal(t, "000000000010/000020/000030", e.Event.ProtocolID)
@@ -1588,6 +1389,34 @@ func TestHandleMessageContractEvent(t *testing.T) {
 	em.AssertExpectations(t)
 }
 
+func TestHandleMessageContractEventNoPayload(t *testing.T) {
+	data := []byte(`
+[
+	{
+		"chaincodeId": "basic",
+	  "blockNumber": 10,
+		"transactionId": "4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d",
+		"transactionIndex": 20,
+		"eventIndex": 30,
+		"eventName": "AssetCreated",
+		"subId": "sb-cb37cc07-e873-4f58-44ab-55add6bba320"
+	}
+]`)
+
+	em := &blockchainmocks.Callbacks{}
+	e := &Fabric{
+		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
+	}
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
+
+	var events []interface{}
+	err := json.Unmarshal(data, &events)
+	assert.NoError(t, err)
+	err = e.handleMessageBatch(context.Background(), events)
+	assert.NoError(t, err)
+}
+
 func TestHandleMessageContractEventBadPayload(t *testing.T) {
 	data := []byte(`
 [
@@ -1605,7 +1434,8 @@ func TestHandleMessageContractEventBadPayload(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-cb37cc07-e873-4f58-44ab-55add6bba320"] = "ns1"
 
 	var events []interface{}
 	err := json.Unmarshal(data, &events)
@@ -1633,7 +1463,8 @@ func TestHandleMessageContractEventError(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-b5b97a4e-a317-4053-6400-1474650efcb5"
+	e.subs = map[string]string{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = "ns1"
 
 	em.On("BlockchainEvent", mock.Anything).Return(fmt.Errorf("pop"))
 
@@ -1988,31 +1819,6 @@ func TestGenerateEventSignature(t *testing.T) {
 	assert.Equal(t, "Changed", signature)
 }
 
-func TestSubmitNetworkAction(t *testing.T) {
-
-	e, cancel := newTestFabric()
-	defer cancel()
-	httpmock.ActivateNonDefault(e.client.GetClient())
-	defer httpmock.DeactivateAndReset()
-
-	signer := "signer001"
-
-	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
-		func(req *http.Request) (*http.Response, error) {
-			var body map[string]interface{}
-			json.NewDecoder(req.Body).Decode(&body)
-			assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"])
-			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"])
-			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"])
-			assert.Equal(t, "", (body["args"].(map[string]interface{}))["payloadRef"])
-			return httpmock.NewJsonResponderOrPanic(200, "")(req)
-		})
-
-	err := e.SubmitNetworkAction(context.Background(), "", signer, core.NetworkActionTerminate)
-	assert.NoError(t, err)
-
-}
-
 func TestHandleNetworkAction(t *testing.T) {
 	data := []byte(`
 [
@@ -2032,7 +1838,9 @@ func TestHandleNetworkAction(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeMSPIdentity,
@@ -2070,7 +1878,8 @@ func TestHandleNetworkActionFail(t *testing.T) {
 	e := &Fabric{
 		callbacks: callbacks{handlers: []blockchain.Callbacks{em}},
 	}
-	e.fireflyContract.subscription = "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"
+	e.subs = map[string]string{}
+	e.subs["sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e"] = "ns1"
 
 	expectedSigningKeyRef := &core.VerifierRef{
 		Type:  core.VerifierTypeMSPIdentity,
@@ -2089,8 +1898,178 @@ func TestHandleNetworkActionFail(t *testing.T) {
 
 }
 
-func TestNetworkVersion(t *testing.T) {
+func TestGetNetworkVersion(t *testing.T) {
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"),
+		mockNetworkVersion(t, 1))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, version)
+}
+
+func TestGetNetworkVersionFunctionNotFound(t *testing.T) {
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
+		httpmock.NewJsonResponderOrPanic(500, fabError{Error: "Function NetworkVersion not found"}))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.NoError(t, err)
+	assert.Equal(t, 1, version)
+}
+
+func TestGetNetworkVersionBadLocation(t *testing.T) {
+	e, _ := newTestFabric()
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "no good",
+	}.String())
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "FF10310", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestGetNetworkVersionBadResponse(t *testing.T) {
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
+		httpmock.NewJsonResponderOrPanic(200, ""))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "json: cannot unmarshal", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestGetNetworkVersionFunctionNotFoundQueryFail(t *testing.T) {
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	httpmock.RegisterResponder("POST", "http://localhost:12345/query",
+		httpmock.NewJsonResponderOrPanic(500, fabError{Error: "pop"}))
+
+	version, err := e.GetNetworkVersion(context.Background(), location)
+
+	assert.Regexp(t, "pop", err)
+	assert.Equal(t, 0, version)
+}
+
+func TestConvertDeprecatedContractConfig(t *testing.T) {
+	e, _ := newTestFabric()
+	resetConf(e)
+	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "Firefly")
+	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
+
+	locationBytes, fromBlock, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Equal(t, "oldest", fromBlock)
+	assert.NoError(t, err)
+
+	location, err := parseContractLocation(e.ctx, locationBytes)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "Firefly", location.Chaincode)
+}
+
+func TestConvertDeprecatedContractConfigNoChaincode(t *testing.T) {
 	e, _ := newTestFabric()
-	e.fireflyContract.networkVersion = 2
-	assert.Equal(t, 2, e.NetworkVersion())
+	resetConf(e)
+	utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
+	utFabconnectConf.Set(FabconnectConfigSigner, "signer001")
+
+	_, _, err := e.GetAndConvertDeprecatedContractConfig(e.ctx)
+	assert.Regexp(t, "F10138", err)
+}
+
+func TestSubmitNetworkAction(t *testing.T) {
+
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	signer := "signer001"
+
+	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"])
+			assert.Equal(t, "", (body["args"].(map[string]interface{}))["payloadRef"])
+			return httpmock.NewJsonResponderOrPanic(200, "")(req)
+		})
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"channel":   "firefly",
+		"chaincode": "simplestorage",
+	}.String())
+
+	err := e.SubmitNetworkAction(context.Background(), "", signer, core.NetworkActionTerminate, location)
+	assert.NoError(t, err)
+}
+
+func TestSubmitNetworkActionBadLocation(t *testing.T) {
+	e, cancel := newTestFabric()
+	defer cancel()
+	httpmock.ActivateNonDefault(e.client.GetClient())
+	defer httpmock.DeactivateAndReset()
+
+	signer := "signer001"
+
+	httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`,
+		func(req *http.Request) (*http.Response, error) {
+			var body map[string]interface{}
+			json.NewDecoder(req.Body).Decode(&body)
+			assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"])
+			assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"])
+			assert.Equal(t, "", (body["args"].(map[string]interface{}))["payloadRef"])
+			return httpmock.NewJsonResponderOrPanic(200, "")(req)
+		})
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"bad": "location",
+	}.String())
+
+	err := e.SubmitNetworkAction(context.Background(), "", signer, core.NetworkActionTerminate, location)
+	assert.Regexp(t, "FF10310", err)
 }
diff --git a/internal/dataexchange/ffdx/ffdx_test.go b/internal/dataexchange/ffdx/ffdx_test.go
index de41c9790..23e788907 100644
--- a/internal/dataexchange/ffdx/ffdx_test.go
+++ b/internal/dataexchange/ffdx/ffdx_test.go
@@ -446,8 +446,7 @@ func TestEvents(t *testing.T) {
 	h.SetHandler(mcb)
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "1" &&
-			ev.NamespacedID() == "tx12345" &&
+		return ev.NamespacedID() == "1" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusFailed &&
@@ -458,7 +457,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"1"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "2" &&
+		return ev.NamespacedID() == "2" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusSucceeded
@@ -468,7 +467,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"2"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "3" &&
+		return ev.NamespacedID() == "3" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusSucceeded &&
@@ -480,7 +479,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"3"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "4" &&
+		return ev.NamespacedID() == "4" &&
 			ev.Type() == dataexchange.DXEventTypeMessageReceived &&
 			ev.MessageReceived().PeerID == "peer1" &&
 			string(ev.MessageReceived().Data) == "message1"
@@ -490,7 +489,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"4","manifest":"{\"manifest\":true}"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "5" &&
+		return ev.NamespacedID() == "5" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusFailed &&
@@ -501,7 +500,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"5"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "6" &&
+		return ev.NamespacedID() == "6" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusSucceeded &&
@@ -522,7 +521,7 @@ func TestEvents(t *testing.T) {
 
 	hash := fftypes.NewRandB32()
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "9" &&
+		return ev.NamespacedID() == "9" &&
 			ev.Type() == dataexchange.DXEventTypePrivateBlobReceived &&
 			ev.PrivateBlobReceived().Hash.Equals(hash)
 	})).Run(acker()).Return(nil)
@@ -531,7 +530,7 @@ func TestEvents(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"9"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "10" &&
+		return ev.NamespacedID() == "10" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().TrackingID == "tx12345" &&
 			ev.TransferResult().Status == core.OpStatusSucceeded &&
@@ -561,7 +560,7 @@ func TestEventsWithManifest(t *testing.T) {
 	h.SetHandler(mcb)
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "1" &&
+		return ev.NamespacedID() == "1" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().Status == core.OpStatusPending
 	})).Run(acker()).Return(nil)
@@ -570,7 +569,7 @@ func TestEventsWithManifest(t *testing.T) {
 	assert.Equal(t, `{"action":"ack","id":"1"}`, string(msg))
 
 	mcb.On("DXEvent", mock.MatchedBy(func(ev dataexchange.DXEvent) bool {
-		return ev.EventID() == "2" &&
+		return ev.NamespacedID() == "2" &&
 			ev.Type() == dataexchange.DXEventTypeTransferResult &&
 			ev.TransferResult().Status == core.OpStatusPending
 	})).Run(acker()).Return(nil)
diff --git a/internal/events/batch_pin_complete_test.go b/internal/events/batch_pin_complete_test.go
index ec2f6b5e7..b2513c429 100644
--- a/internal/events/batch_pin_complete_test.go
+++ b/internal/events/batch_pin_complete_test.go
@@ -23,7 +23,6 @@ import (
 	"testing"
 
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
-	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
 	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
 	"github.com/hyperledger/firefly/mocks/shareddownloadmocks"
@@ -137,9 +136,8 @@ func TestBatchPinCompleteOkBroadcast(t *testing.T) {
 	msd := em.sharedDownload.(*shareddownloadmocks.Manager)
 	mdi.On("GetBatchByID", mock.Anything, "ns1", mock.Anything).Return(nil, nil)
 	msd.On("InitiateDownloadBatch", mock.Anything, "ns1", batchPin.TransactionID, batchPin.BatchPayloadRef).Return(nil)
-	mbi := &blockchainmocks.Plugin{}
 
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0x12345",
 	})
@@ -207,9 +205,8 @@ func TestBatchPinCompleteOkBroadcastExistingBatch(t *testing.T) {
 	})).Return(nil).Times(1)
 	mdi.On("InsertPins", mock.Anything, mock.Anything).Return(nil).Once()
 	mdi.On("GetBatchByID", mock.Anything, "ns1", mock.Anything).Return(batchPersisted, nil)
-	mbi := &blockchainmocks.Plugin{}
 
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0x12345",
 	})
@@ -246,9 +243,7 @@ func TestBatchPinCompleteOkPrivate(t *testing.T) {
 	mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil)
 	mdi.On("GetBatchByID", mock.Anything, "ns1", mock.Anything).Return(nil, nil)
 
-	mbi := &blockchainmocks.Plugin{}
-
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0xffffeeee",
 	})
@@ -290,9 +285,7 @@ func TestBatchPinCompleteInsertPinsFail(t *testing.T) {
 	mth.On("InsertBlockchainEvent", mock.Anything, mock.Anything).Return(nil)
 	mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil)
 
-	mbi := &blockchainmocks.Plugin{}
-
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0xffffeeee",
 	})
@@ -328,9 +321,7 @@ func TestBatchPinCompleteGetBatchByIDFails(t *testing.T) {
 	mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil)
 	mdi.On("GetBatchByID", mock.Anything, "ns1", mock.Anything).Return(nil, fmt.Errorf("batch lookup failed"))
 
-	mbi := &blockchainmocks.Plugin{}
-
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0xffffeeee",
 	})
@@ -356,7 +347,6 @@ func TestSequencedBroadcastInitiateDownloadFail(t *testing.T) {
 	}
 
 	cancel() // to avoid retry
-	mbi := &blockchainmocks.Plugin{}
 
 	mth := em.txHelper.(*txcommonmocks.Helper)
 	mth.On("PersistTransaction", mock.Anything, batchPin.TransactionID, core.TransactionTypeBatchPin, "0x12345").Return(true, nil)
@@ -370,7 +360,7 @@ func TestSequencedBroadcastInitiateDownloadFail(t *testing.T) {
 	msd := em.sharedDownload.(*shareddownloadmocks.Manager)
 	msd.On("InitiateDownloadBatch", mock.Anything, "ns1", batchPin.TransactionID, batchPin.BatchPayloadRef).Return(fmt.Errorf("pop"))
 
-	err := em.BatchPinComplete(mbi, batchPin, &core.VerifierRef{
+	err := em.BatchPinComplete(batchPin, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0xffffeeee",
 	})
@@ -385,9 +375,8 @@ func TestBatchPinCompleteNoTX(t *testing.T) {
 	defer cancel()
 
 	batch := &blockchain.BatchPin{}
-	mbi := &blockchainmocks.Plugin{}
 
-	err := em.BatchPinComplete(mbi, batch, &core.VerifierRef{
+	err := em.BatchPinComplete(batch, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0x12345",
 	})
@@ -405,9 +394,8 @@ func TestBatchPinCompleteBadNamespace(t *testing.T) {
 			BlockchainTXID: "0x12345",
 		},
 	}
-	mbi := &blockchainmocks.Plugin{}
 
-	err := em.BatchPinComplete(mbi, batch, &core.VerifierRef{
+	err := em.BatchPinComplete(batch, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0x12345",
 	})
@@ -425,9 +413,8 @@ func TestBatchPinCompleteWrongNamespace(t *testing.T) {
 			BlockchainTXID: "0x12345",
 		},
 	}
-	mbi := &blockchainmocks.Plugin{}
 
-	err := em.BatchPinComplete(mbi, batch, &core.VerifierRef{
+	err := em.BatchPinComplete(batch, &core.VerifierRef{
 		Type:  core.VerifierTypeEthAddress,
 		Value: "0x12345",
 	})
diff --git a/internal/events/event_manager_test.go b/internal/events/event_manager_test.go
index 94b26d92a..218aed94c 100644
--- a/internal/events/event_manager_test.go
+++ b/internal/events/event_manager_test.go
@@ -35,6 +35,7 @@ import (
 	"github.com/hyperledger/firefly/mocks/eventsmocks"
 	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/privatemessagingmocks"
 	"github.com/hyperledger/firefly/mocks/shareddownloadmocks"
 	"github.com/hyperledger/firefly/mocks/sharedstoragemocks"
@@ -83,6 +84,7 @@ func newTestEventManagerCommon(t *testing.T, metrics, dbconcurrency bool) (*even
 	mmi := &metricsmocks.Manager{}
 	mev := &eventsmocks.Plugin{}
 	events := map[string]events.Plugin{"websockets": mev}
+	mmp := &multipartymocks.Manager{}
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
 	mmi.On("IsMetricsEnabled").Return(metrics)
 	if metrics {
@@ -94,7 +96,7 @@ func newTestEventManagerCommon(t *testing.T, metrics, dbconcurrency bool) (*even
 	mdi.On("Capabilities").Return(&database.Capabilities{Concurrency: dbconcurrency}).Maybe()
 	mev.On("SetHandler", "ns1", mock.Anything).Return(nil).Maybe()
 	mev.On("ValidateOptions", mock.Anything).Return(nil).Maybe()
-	emi, err := NewEventManager(ctx, "ns1", mni, mpi, mdi, mbi, mim, msh, mdm, mbm, mpm, mam, mdd, mmi, txHelper, events)
+	emi, err := NewEventManager(ctx, "ns1", mni, mpi, mdi, mbi, mim, msh, mdm, mbm, mpm, mam, mdd, mmi, txHelper, events, mmp)
 	em := emi.(*eventManager)
 	em.txHelper = &txcommonmocks.Helper{}
 	mockRunAsGroupPassthrough(mdi)
@@ -129,7 +131,7 @@ func TestStartStop(t *testing.T) {
 }
 
 func TestStartStopBadDependencies(t *testing.T) {
-	_, err := NewEventManager(context.Background(), "", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	_, err := NewEventManager(context.Background(), "", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 
 }
@@ -151,11 +153,12 @@ func TestStartStopEventListenerFail(t *testing.T) {
 	mm := &metricsmocks.Manager{}
 	mev := &eventsmocks.Plugin{}
 	events := map[string]events.Plugin{"websockets": mev}
+	mmp := &multipartymocks.Manager{}
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
 	mdi.On("Capabilities").Return(&database.Capabilities{Concurrency: false})
 	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress)
 	mev.On("SetHandler", "ns1", mock.Anything).Return(fmt.Errorf("pop"))
-	_, err := NewEventManager(context.Background(), "ns1", mni, mpi, mdi, mbi, mim, msh, mdm, mbm, mpm, mam, msd, mm, txHelper, events)
+	_, err := NewEventManager(context.Background(), "ns1", mni, mpi, mdi, mbi, mim, msh, mdm, mbm, mpm, mam, msd, mm, txHelper, events, mmp)
 	assert.EqualError(t, err, "pop")
 }
 
diff --git a/internal/events/network_action_test.go b/internal/events/network_action_test.go
index a3ce94268..b9b3fbab4 100644
--- a/internal/events/network_action_test.go
+++ b/internal/events/network_action_test.go
@@ -21,9 +21,9 @@ import (
 	"testing"
 
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
-	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
 	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/txcommonmocks"
 	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
@@ -41,7 +41,7 @@ func TestNetworkAction(t *testing.T) {
 		Value: "0x1234",
 	}
 
-	mbi := &blockchainmocks.Plugin{}
+	mmp := em.multiparty.(*multipartymocks.Manager)
 	mdi := em.database.(*databasemocks.Plugin)
 	mth := em.txHelper.(*txcommonmocks.Helper)
 	mii := em.identity.(*identitymanagermocks.Manager)
@@ -54,12 +54,11 @@ func TestNetworkAction(t *testing.T) {
 	mdi.On("InsertEvent", em.ctx, mock.Anything).Return(nil)
 	mdi.On("GetNamespace", em.ctx, "ns1").Return(&core.Namespace{}, nil)
 	mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
-	mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil)
+	mmp.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil)
 
-	err := em.BlockchainNetworkAction(mbi, "terminate", event, verifier)
+	err := em.BlockchainNetworkAction("terminate", event, verifier)
 	assert.NoError(t, err)
 
-	mbi.AssertExpectations(t)
 	mdi.AssertExpectations(t)
 	mth.AssertExpectations(t)
 	mii.AssertExpectations(t)
@@ -74,16 +73,14 @@ func TestNetworkActionUnknownIdentity(t *testing.T) {
 		Value: "0x1234",
 	}
 
-	mbi := &blockchainmocks.Plugin{}
 	mii := em.identity.(*identitymanagermocks.Manager)
 
 	mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, verifier).Return(nil, fmt.Errorf("pop")).Once()
 	mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, verifier).Return(nil, nil).Once()
 
-	err := em.BlockchainNetworkAction(mbi, "terminate", &blockchain.Event{}, verifier)
+	err := em.BlockchainNetworkAction("terminate", &blockchain.Event{}, verifier)
 	assert.NoError(t, err)
 
-	mbi.AssertExpectations(t)
 	mii.AssertExpectations(t)
 }
 
@@ -96,7 +93,6 @@ func TestNetworkActionNonRootIdentity(t *testing.T) {
 		Value: "0x1234",
 	}
 
-	mbi := &blockchainmocks.Plugin{}
 	mii := em.identity.(*identitymanagermocks.Manager)
 
 	mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, verifier).Return(&core.Identity{
@@ -105,10 +101,9 @@ func TestNetworkActionNonRootIdentity(t *testing.T) {
 		},
 	}, nil)
 
-	err := em.BlockchainNetworkAction(mbi, "terminate", &blockchain.Event{}, verifier)
+	err := em.BlockchainNetworkAction("terminate", &blockchain.Event{}, verifier)
 	assert.NoError(t, err)
 
-	mbi.AssertExpectations(t)
 	mii.AssertExpectations(t)
 }
 
@@ -121,15 +116,13 @@ func TestNetworkActionUnknown(t *testing.T) {
 		Value: "0x1234",
 	}
 
-	mbi := &blockchainmocks.Plugin{}
 	mii := em.identity.(*identitymanagermocks.Manager)
 
 	mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, verifier).Return(&core.Identity{}, nil)
 
-	err := em.BlockchainNetworkAction(mbi, "bad", &blockchain.Event{}, verifier)
+	err := em.BlockchainNetworkAction("bad", &blockchain.Event{}, verifier)
 	assert.NoError(t, err)
 
-	mbi.AssertExpectations(t)
 	mii.AssertExpectations(t)
 }
 
@@ -137,15 +130,15 @@ func TestActionTerminateQueryFail(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
 
-	mbi := &blockchainmocks.Plugin{}
+	mmp := &multipartymocks.Manager{}
 	mdi := em.database.(*databasemocks.Plugin)
 
 	mdi.On("GetNamespace", em.ctx, "ns1").Return(nil, fmt.Errorf("pop"))
 
-	err := em.actionTerminate(mbi, &blockchain.Event{})
+	err := em.actionTerminate(mmp, &blockchain.Event{})
 	assert.EqualError(t, err, "pop")
 
-	mbi.AssertExpectations(t)
+	mmp.AssertExpectations(t)
 	mdi.AssertExpectations(t)
 }
 
@@ -153,16 +146,16 @@ func TestActionTerminateFail(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
 
-	mbi := &blockchainmocks.Plugin{}
+	mmp := &multipartymocks.Manager{}
 	mdi := em.database.(*databasemocks.Plugin)
 
 	mdi.On("GetNamespace", em.ctx, "ns1").Return(&core.Namespace{}, nil)
-	mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(fmt.Errorf("pop"))
+	mmp.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(fmt.Errorf("pop"))
 
-	err := em.actionTerminate(mbi, &blockchain.Event{})
+	err := em.actionTerminate(mmp, &blockchain.Event{})
 	assert.EqualError(t, err, "pop")
 
-	mbi.AssertExpectations(t)
+	mmp.AssertExpectations(t)
 	mdi.AssertExpectations(t)
 }
 
@@ -170,16 +163,16 @@ func TestActionTerminateUpsertFail(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
 
-	mbi := &blockchainmocks.Plugin{}
+	mmp := &multipartymocks.Manager{}
 	mdi := em.database.(*databasemocks.Plugin)
 
 	mdi.On("GetNamespace", em.ctx, "ns1").Return(&core.Namespace{}, nil)
 	mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(fmt.Errorf("pop"))
-	mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil)
+	mmp.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil)
 
-	err := em.actionTerminate(mbi, &blockchain.Event{})
+	err := em.actionTerminate(mmp, &blockchain.Event{})
 	assert.EqualError(t, err, "pop")
 
-	mbi.AssertExpectations(t)
+	mmp.AssertExpectations(t)
 	mdi.AssertExpectations(t)
 }
diff --git a/internal/identity/identitymanager_test.go b/internal/identity/identitymanager_test.go
index 58a53406a..b65fcf510 100644
--- a/internal/identity/identitymanager_test.go
+++ b/internal/identity/identitymanager_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/databasemocks"
 	"github.com/hyperledger/firefly/mocks/datamocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -37,17 +38,18 @@ func newTestIdentityManager(t *testing.T) (context.Context, *identityManager) {
 	mdi := &databasemocks.Plugin{}
 	mbi := &blockchainmocks.Plugin{}
 	mdm := &datamocks.Manager{}
+	mmp := &multipartymocks.Manager{}
 
 	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress).Maybe()
 
 	ctx := context.Background()
-	im, err := NewIdentityManager(ctx, "ns1", "", "", "", mdi, mbi, mdm)
+	im, err := NewIdentityManager(ctx, "ns1", "", "", "", mdi, mbi, mdm, mmp)
 	assert.NoError(t, err)
 	return ctx, im.(*identityManager)
 }
 
 func TestNewIdentityManagerMissingDeps(t *testing.T) {
-	_, err := NewIdentityManager(context.Background(), "", "", "", "", nil, nil, nil)
+	_, err := NewIdentityManager(context.Background(), "", "", "", "", nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
@@ -153,8 +155,9 @@ func TestResolveInputSigningIdentityAnonymousKeyWithAuthorOk(t *testing.T) {
 	ctx, im := newTestIdentityManager(t)
 
 	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mmp := im.multiparty.(*multipartymocks.Manager)
 	mbi.On("NormalizeSigningKey", ctx, "mykey123").Return("fullkey123", nil)
-	mbi.On("NetworkVersion").Return(1)
+	mmp.On("GetNetworkVersion").Return(1)
 
 	idID := fftypes.NewUUID()
 
@@ -192,8 +195,9 @@ func TestResolveInputSigningIdentityKeyWithNoAuthorFail(t *testing.T) {
 	ctx, im := newTestIdentityManager(t)
 
 	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mmp := im.multiparty.(*multipartymocks.Manager)
 	mbi.On("NormalizeSigningKey", ctx, "mykey123").Return("fullkey123", nil)
-	mbi.On("NetworkVersion").Return(1)
+	mmp.On("GetNetworkVersion").Return(1)
 
 	mdi := im.database.(*databasemocks.Plugin)
 	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, "ns1", "fullkey123").Return(nil, nil)
@@ -208,7 +212,7 @@ func TestResolveInputSigningIdentityKeyWithNoAuthorFail(t *testing.T) {
 
 	mbi.AssertExpectations(t)
 	mdi.AssertExpectations(t)
-
+	mmp.AssertExpectations(t)
 }
 
 func TestResolveInputSigningIdentityByKeyDIDMismatch(t *testing.T) {
@@ -258,8 +262,9 @@ func TestResolveInputSigningIdentityByKeyNotFound(t *testing.T) {
 	ctx, im := newTestIdentityManager(t)
 
 	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mmp := im.multiparty.(*multipartymocks.Manager)
 	mbi.On("NormalizeSigningKey", ctx, "mykey123").Return("fullkey123", nil)
-	mbi.On("NetworkVersion").Return(1)
+	mmp.On("GetNetworkVersion").Return(1)
 
 	mdi := im.database.(*databasemocks.Plugin)
 	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, "ns1", "fullkey123").
@@ -547,7 +552,8 @@ func TestResolveDefaultSigningIdentityNotFound(t *testing.T) {
 	}
 
 	mbi := im.blockchain.(*blockchainmocks.Plugin)
-	mbi.On("NetworkVersion").Return(1)
+	mmp := im.multiparty.(*multipartymocks.Manager)
+	mmp.On("GetNetworkVersion").Return(1)
 
 	mdi := im.database.(*databasemocks.Plugin)
 	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, "ns1", "key12345").Return(nil, nil)
@@ -586,7 +592,8 @@ func TestResolveDefaultSigningIdentitySystemFallback(t *testing.T) {
 	}
 
 	mbi := im.blockchain.(*blockchainmocks.Plugin)
-	mbi.On("NetworkVersion").Return(1)
+	mmp := im.multiparty.(*multipartymocks.Manager)
+	mmp.On("GetNetworkVersion").Return(1)
 
 	mdi := im.database.(*databasemocks.Plugin)
 	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, "ns1", "key12345").Return(nil, nil)
diff --git a/internal/multiparty/manager_test.go b/internal/multiparty/manager_test.go
new file mode 100644
index 000000000..5e32fc105
--- /dev/null
+++ b/internal/multiparty/manager_test.go
@@ -0,0 +1,353 @@
+// 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 (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/mocks/blockchainmocks"
+	"github.com/hyperledger/firefly/pkg/blockchain"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+type testMultipartyManager struct {
+	multipartyManager
+	mbi *blockchainmocks.Plugin
+}
+
+func newTestMultipartyManager() *testMultipartyManager {
+	nm := &testMultipartyManager{
+		mbi: &blockchainmocks.Plugin{},
+		multipartyManager: multipartyManager{
+			ctx: context.Background(),
+		},
+	}
+
+	nm.multipartyManager.blockchain = nm.mbi
+	nm.mbi.On("Name").Return("ethereum")
+	nm.mbi.On("SubmitBatchPin", context.Background(), mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mbi.On("SubmitNetworkAction", context.Background(), mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	return nm
+}
+
+func TestNewMultipartyManager(t *testing.T) {
+	mbi := &blockchainmocks.Plugin{}
+	contracts := make([]Contract, 0)
+	nm := NewMultipartyManager(context.Background(), "namespace", contracts, mbi)
+	assert.NotNil(t, nm)
+}
+
+func TestConfigureContract(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+}
+
+func TestConfigureContractOldestBlock(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "0", mp.activeContract.firstEvent)
+}
+
+func TestConfigureContractNewestBlock(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "latest",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "latest", mp.activeContract.firstEvent)
+}
+
+func TestResolveContractDeprecatedConfig(t *testing.T) {
+	mp := newTestMultipartyManager()
+	mp.contracts = []Contract{}
+
+	mp.mbi.On("GetAndConvertDeprecatedContractConfig", context.Background()).Return(fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String()), "0", nil)
+
+	loc, firstBlock, err := mp.resolveFireFlyContract(context.Background(), 0)
+
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	assert.Equal(t, location, loc)
+	assert.Equal(t, "0", firstBlock)
+	assert.NoError(t, err)
+}
+
+func TestResolveContractDeprecatedConfigError(t *testing.T) {
+	mp := newTestMultipartyManager()
+	mp.contracts = []Contract{}
+
+	mp.mbi.On("GetAndConvertDeprecatedContractConfig", context.Background()).Return(nil, "", fmt.Errorf("pop"))
+
+	_, _, err := mp.resolveFireFlyContract(context.Background(), 0)
+	assert.Regexp(t, "pop", err)
+}
+
+func TestResolveContractDeprecatedConfigNewestBlock(t *testing.T) {
+	mp := newTestMultipartyManager()
+	mp.contracts = []Contract{}
+
+	mp.mbi.On("GetAndConvertDeprecatedContractConfig", context.Background()).Return(fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String()), "newst", nil)
+
+	_, _, err := mp.resolveFireFlyContract(context.Background(), 0)
+	assert.NoError(t, err)
+}
+
+func TestConfigureContractBadIndex(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 1},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.Regexp(t, "FF10396", err)
+}
+
+func TestConfigureContractNetworkVersionFail(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(0, fmt.Errorf("pop"))
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.Regexp(t, "pop", err)
+}
+
+func TestGetBlockchainName(t *testing.T) {
+	mp := newTestMultipartyManager()
+	name := mp.Name()
+	assert.Equal(t, "ethereum", name)
+}
+
+func TestSubmitNetworkAction(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+	err = mp.SubmitNetworkAction(context.Background(), "test", "0x123", core.NetworkActionTerminate)
+	assert.Nil(t, err)
+}
+
+func TestSubmitBatchPin(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	batchPin := &blockchain.BatchPin{}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+	err = mp.SubmitBatchPin(context.Background(), "ns1", "0x123", batchPin)
+	assert.Nil(t, err)
+}
+
+func TestGetNetworkVersion(t *testing.T) {
+	contracts := make([]Contract, 1)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+
+	version := mp.GetNetworkVersion()
+	assert.Equal(t, 1, version)
+}
+
+func TestConfgureAndTerminateContract(t *testing.T) {
+	contracts := make([]Contract, 2)
+	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String())
+	contract := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+	contract2 := Contract{
+		FirstEvent: "0",
+		Location:   location,
+	}
+
+	contracts[0] = contract
+	contracts[1] = contract2
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+	mp.mbi.On("RemoveFireflySubscription", mock.Anything, mock.Anything).Return(nil)
+	mp.multipartyManager.contracts = contracts
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+	err := mp.ConfigureContract(context.Background(), cf)
+	assert.NoError(t, err)
+
+	err = mp.TerminateContract(context.Background(), cf, &blockchain.Event{})
+	assert.NoError(t, err)
+}
+
+func TestTerminateContractError(t *testing.T) {
+	mp := newTestMultipartyManager()
+	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+	mp.mbi.On("RemoveFireflySubscription", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
+
+	cf := &core.FireFlyContracts{
+		Active: core.FireFlyContractInfo{Index: 0},
+	}
+
+	err := mp.TerminateContract(context.Background(), cf, &blockchain.Event{})
+	assert.Regexp(t, "pop", err)
+}
diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go
index 60976473e..f591ab5b0 100644
--- a/internal/namespace/manager_test.go
+++ b/internal/namespace/manager_test.go
@@ -37,6 +37,7 @@ import (
 	"github.com/hyperledger/firefly/mocks/eventsmocks"
 	"github.com/hyperledger/firefly/mocks/identitymocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"github.com/hyperledger/firefly/mocks/orchestratormocks"
 	"github.com/hyperledger/firefly/mocks/sharedstoragemocks"
@@ -128,7 +129,10 @@ func TestInit(t *testing.T) {
 	defer nm.cleanup(t)
 
 	mo := &orchestratormocks.Orchestrator{}
+	mmp := &multipartymocks.Manager{}
 	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
+	mo.On("MultiParty").Return(mmp)
+	mmp.On("GetNetworkVersion").Return(2)
 	nm.utOrchestrator = mo
 
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
@@ -138,7 +142,6 @@ func TestInit(t *testing.T) {
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
-	nm.mbi.On("NetworkVersion").Return(2)
 
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	err := nm.Init(ctx, cancelCtx)
@@ -271,11 +274,16 @@ func TestInitVersion1(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
+	mmp := &multipartymocks.Manager{}
 	mo := &orchestratormocks.Orchestrator{}
 	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
 	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
 	nm.utOrchestrator = mo
 
+	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
+	mo.On("MultiParty").Return(mmp)
+	mmp.On("GetNetworkVersion").Return(1)
+
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("SetHandler", mock.Anything).Return()
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
@@ -283,7 +291,6 @@ func TestInitVersion1(t *testing.T) {
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
-	nm.mbi.On("NetworkVersion").Return(1)
 
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	err := nm.Init(ctx, cancelCtx)
@@ -301,10 +308,15 @@ func TestInitVersion1Fail(t *testing.T) {
 	defer nm.cleanup(t)
 
 	mo := &orchestratormocks.Orchestrator{}
+	mmp := &multipartymocks.Manager{}
 	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
 	mo.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Once()
 	nm.utOrchestrator = mo
 
+	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
+	mo.On("MultiParty").Return(mmp)
+	mmp.On("GetNetworkVersion").Return(1)
+
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("SetHandler", mock.Anything).Return()
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
@@ -312,7 +324,6 @@ func TestInitVersion1Fail(t *testing.T) {
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
-	nm.mbi.On("NetworkVersion").Return(1)
 
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	err := nm.Init(ctx, cancelCtx)
@@ -932,6 +943,40 @@ func TestLoadNamespacesMultipartyMultipleDB(t *testing.T) {
 	assert.Regexp(t, "FF10394.*database", err)
 }
 
+func TestLoadNamespacesMultipartyContract(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+  namespaces:
+    default: ns1
+    predefined:
+    - name: ns1
+      multiparty:
+        enabled: true
+        contract:
+        - location:
+            address: 0x4ae50189462b0e5d52285f59929d037f790771a6 
+          firstEvent: oldest
+  `))
+	assert.NoError(t, err)
+
+	err = nm.loadNamespaces(context.Background())
+	assert.NoError(t, err)
+}
+
+func TestLoadNamespacesMultipartyContractBadLocation(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	fail := make(chan string)
+	namespaceConfig.AddKnownKey("predefined.0.multiparty.contract.0.location.address", fail)
+
+	err := nm.loadNamespaces(context.Background())
+	assert.Regexp(t, "json:", err)
+}
+
 func TestLoadNamespacesGatewayMultipleDB(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go
index d8809ffb0..dfdf3fc3b 100644
--- a/internal/orchestrator/bound_callbacks_test.go
+++ b/internal/orchestrator/bound_callbacks_test.go
@@ -25,6 +25,7 @@ import (
 	"github.com/hyperledger/firefly/mocks/blockchainmocks"
 	"github.com/hyperledger/firefly/mocks/dataexchangemocks"
 	"github.com/hyperledger/firefly/mocks/eventmocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"github.com/hyperledger/firefly/mocks/sharedstoragemocks"
 	"github.com/hyperledger/firefly/mocks/tokenmocks"
@@ -39,11 +40,12 @@ import (
 func TestBoundCallbacks(t *testing.T) {
 	mei := &eventmocks.EventManager{}
 	mbi := &blockchainmocks.Plugin{}
+	mmp := &multipartymocks.Manager{}
 	mdx := &dataexchangemocks.Plugin{}
 	mti := &tokenmocks.Plugin{}
 	mss := &sharedstoragemocks.Plugin{}
 	mom := &operationmocks.Manager{}
-	bc := boundCallbacks{bi: mbi, dx: mdx, ei: mei, ss: mss, om: mom}
+	bc := boundCallbacks{dx: mdx, ei: mei, ss: mss, om: mom, multiparty: mmp}
 
 	info := fftypes.JSONObject{"hello": "world"}
 	batch := &blockchain.BatchPin{TransactionID: fftypes.NewUUID()}
@@ -54,11 +56,11 @@ func TestBoundCallbacks(t *testing.T) {
 	hash := fftypes.NewRandB32()
 	opID := fftypes.NewUUID()
 
-	mei.On("BatchPinComplete", mbi, batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
+	mei.On("BatchPinComplete", batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
 	err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress})
 	assert.EqualError(t, err, "pop")
 
-	mei.On("BlockchainNetworkAction", mbi, "terminate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
+	mei.On("BlockchainNetworkAction", "terminate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
 	err = bc.BlockchainNetworkAction("terminate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress})
 	assert.EqualError(t, err, "pop")
 
diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index f1dd35919..3c3d61bd6 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -37,6 +37,7 @@ import (
 	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
 	"github.com/hyperledger/firefly/mocks/identitymocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
+	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/networkmapmocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"github.com/hyperledger/firefly/mocks/privatemessagingmocks"
@@ -77,6 +78,7 @@ type testOrchestrator struct {
 	msd *shareddownloadmocks.Manager
 	mae *spieventsmocks.Manager
 	mdh *definitionsmocks.DefinitionHandler
+	mmp *multipartymocks.Manager
 }
 
 func (tor *testOrchestrator) cleanup(t *testing.T) {
@@ -102,6 +104,7 @@ func (tor *testOrchestrator) cleanup(t *testing.T) {
 	tor.msd.AssertExpectations(t)
 	tor.mae.AssertExpectations(t)
 	tor.mdh.AssertExpectations(t)
+	tor.mmp.AssertExpectations(t)
 }
 
 func newTestOrchestrator() *testOrchestrator {
@@ -135,7 +138,9 @@ func newTestOrchestrator() *testOrchestrator {
 		msd: &shareddownloadmocks.Manager{},
 		mae: &spieventsmocks.Manager{},
 		mdh: &definitionsmocks.DefinitionHandler{},
+		mmp: &multipartymocks.Manager{},
 	}
+	tor.orchestrator.multiparty = tor.mmp
 	tor.orchestrator.data = tor.mdm
 	tor.orchestrator.batch = tor.mba
 	tor.orchestrator.broadcast = tor.mbm
@@ -169,6 +174,7 @@ func newTestOrchestrator() *testOrchestrator {
 	tor.mti.On("Name").Return("mock-tk").Maybe()
 	tor.mcm.On("Name").Return("mock-cm").Maybe()
 	tor.mmi.On("Name").Return("mock-mm").Maybe()
+	tor.mmp.On("Name").Return("mock-mp").Maybe()
 	return tor
 }
 
@@ -204,6 +210,7 @@ func TestInitOK(t *testing.T) {
 	assert.Equal(t, or.mom, or.Operations())
 	assert.Equal(t, or.mcm, or.Contracts())
 	assert.Equal(t, or.mnm, or.NetworkMap())
+	assert.Equal(t, or.mmp, or.MultiParty())
 }
 
 func TestInitTokenListenerFail(t *testing.T) {
@@ -309,6 +316,7 @@ func TestInitIdentityComponentFail(t *testing.T) {
 	or.plugins.Database.Plugin = nil
 	or.identity = nil
 	or.txHelper = nil
+	or.multiparty = nil
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -364,7 +372,7 @@ func TestStartBatchFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.mdi.On("GetNamespace", mock.Anything, "ns").Return(nil, nil)
 	or.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil)
-	or.mbi.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	or.mbi.On("Start").Return(nil)
 	or.mba.On("Start").Return(fmt.Errorf("pop"))
 	err := or.Start()
@@ -377,7 +385,7 @@ func TestStartTokensFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.mdi.On("GetNamespace", mock.Anything, "ns").Return(nil, nil)
 	or.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil)
-	or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
+	or.mmp.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
 	or.mbi.On("Start").Return(nil)
 	or.mba.On("Start").Return(nil)
 	or.mem.On("Start").Return(nil)
@@ -395,7 +403,7 @@ func TestStartBlockchainsFail(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
 	or.mdi.On("GetNamespace", mock.Anything, "ns").Return(nil, nil)
-	or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
+	or.mmp.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
 	or.mbi.On("Start").Return(fmt.Errorf("pop"))
 	err := or.Start()
 	assert.EqualError(t, err, "pop")
@@ -406,7 +414,7 @@ func TestStartBlockchainsConfigureFail(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
 	or.mdi.On("GetNamespace", mock.Anything, "ns").Return(nil, nil)
-	or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(fmt.Errorf("pop"))
+	or.mmp.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(fmt.Errorf("pop"))
 	err := or.Start()
 	assert.EqualError(t, err, "pop")
 }
@@ -417,7 +425,7 @@ func TestStartStopOk(t *testing.T) {
 	defer or.cleanup(t)
 	or.mdi.On("GetNamespace", mock.Anything, "ns").Return(nil, nil)
 	or.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil)
-	or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
+	or.mmp.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil)
 	or.mbi.On("Start").Return(nil)
 	or.mba.On("Start").Return(nil)
 	or.mem.On("Start").Return(nil)
@@ -441,7 +449,7 @@ func TestNetworkAction(t *testing.T) {
 	or := newTestOrchestrator()
 	or.namespace = core.LegacySystemNamespace
 	or.mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x123", nil)
-	or.mbi.On("SubmitNetworkAction", context.Background(), mock.Anything, "0x123", core.NetworkActionTerminate).Return(nil)
+	or.mmp.On("SubmitNetworkAction", context.Background(), mock.Anything, "0x123", core.NetworkActionTerminate).Return(nil)
 	err := or.SubmitNetworkAction(context.Background(), &core.NetworkAction{Type: core.NetworkActionTerminate})
 	assert.NoError(t, err)
 }
diff --git a/pkg/core/namespace_test.go b/pkg/core/namespace_test.go
index 769fbecd3..58efb163b 100644
--- a/pkg/core/namespace_test.go
+++ b/pkg/core/namespace_test.go
@@ -55,14 +55,22 @@ func TestNamespaceValidation(t *testing.T) {
 func TestFireFlyContractsDatabaseSerialization(t *testing.T) {
 	contracts1 := &FireFlyContracts{
 		Active: FireFlyContractInfo{
-			Index: 1,
-			Info:  fftypes.JSONObject{"address": "0x1234"},
+			Index:        1,
+			FirstEvent:   "oldest",
+			Subscription: "1234",
+			Location: fftypes.JSONAnyPtr(fftypes.JSONObject{
+				"address": "0x123",
+			}.String()),
 		},
 		Terminated: []FireFlyContractInfo{
 			{
-				Index:      0,
-				Info:       fftypes.JSONObject{"address": "0x0000"},
-				FinalEvent: "50",
+				Index:        0,
+				FinalEvent:   "50",
+				Subscription: "12345",
+				FirstEvent:   "oldest",
+				Location: fftypes.JSONAnyPtr(fftypes.JSONObject{
+					"address": "0x1234",
+				}.String()),
 			},
 		},
 	}
@@ -70,14 +78,16 @@ func TestFireFlyContractsDatabaseSerialization(t *testing.T) {
 	// Verify it serializes as bytes to the database
 	val1, err := contracts1.Value()
 	assert.NoError(t, err)
-	assert.Equal(t, `{"active":{"index":1,"info":{"address":"0x1234"}},"terminated":[{"index":0,"finalEvent":"50","info":{"address":"0x0000"}}]}`, string(val1.([]byte)))
+	assert.Equal(t, `{"active":{"index":1,"location":{"address":"0x123"},"firstEvent":"oldest","subscription":"1234"},"terminated":[{"index":0,"finalEvent":"50","location":{"address":"0x1234"},"firstEvent":"oldest","subscription":"12345"}]}`, string(val1.([]byte)))
 
 	// Verify it restores ok
 	contracts2 := &FireFlyContracts{}
 	err = contracts2.Scan(val1)
 	assert.NoError(t, err)
 	assert.Equal(t, 1, contracts2.Active.Index)
-	assert.Equal(t, fftypes.JSONObject{"address": "0x1234"}, contracts2.Active.Info)
+	assert.Equal(t, *fftypes.JSONAnyPtr(fftypes.JSONObject{
+		"address": "0x123",
+	}.String()), *contracts2.Active.Location)
 	assert.Len(t, contracts2.Terminated, 1)
 
 	// Verify it ignores a blank string
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index 52de56510..33c5b9776 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -437,26 +437,3 @@ func checkObject(t *testing.T, expected interface{}, actual interface{}) bool {
 	}
 	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/ethereum_contract_test.go b/test/e2e/ethereum_contract_test.go
index cd549c514..d794bcb6f 100644
--- a/test/e2e/ethereum_contract_test.go
+++ b/test/e2e/ethereum_contract_test.go
@@ -136,10 +136,6 @@ func (suite *EthereumContractTestSuite) BeforeTest(suiteName, testName string) {
 	suite.testState = beforeE2ETest(suite.T())
 }
 
-func (suite *EthereumContractTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
-}
-
 func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 	defer suite.testState.done()
 
diff --git a/test/e2e/fabric_contract_test.go b/test/e2e/fabric_contract_test.go
index 3c99c42c3..bbed1341b 100644
--- a/test/e2e/fabric_contract_test.go
+++ b/test/e2e/fabric_contract_test.go
@@ -138,10 +138,6 @@ func (suite *FabricContractTestSuite) BeforeTest(suiteName, testName string) {
 	suite.testState = beforeE2ETest(suite.T())
 }
 
-func (suite *FabricContractTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
-}
-
 func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 	defer suite.testState.done()
 
diff --git a/test/e2e/identity_test.go b/test/e2e/identity_test.go
index 44fda88cc..81e0c8f76 100644
--- a/test/e2e/identity_test.go
+++ b/test/e2e/identity_test.go
@@ -22,7 +22,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/stretchr/testify/assert"
@@ -39,10 +38,6 @@ func (suite *IdentityTestSuite) BeforeTest(suiteName, testName string) {
 	suite.testState = beforeE2ETest(suite.T())
 }
 
-func (suite *IdentityTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
-}
-
 func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 	defer suite.testState.done()
 
diff --git a/test/e2e/onchain_offchain_test.go b/test/e2e/onchain_offchain_test.go
index 2bde1cebd..015fc4868 100644
--- a/test/e2e/onchain_offchain_test.go
+++ b/test/e2e/onchain_offchain_test.go
@@ -44,10 +44,6 @@ func (suite *OnChainOffChainTestSuite) BeforeTest(suiteName, testName string) {
 	suite.testState = beforeE2ETest(suite.T())
 }
 
-func (suite *OnChainOffChainTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
-}
-
 func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 	defer suite.testState.done()
 
diff --git a/test/e2e/restclient_test.go b/test/e2e/restclient_test.go
index 953a6eb0b..db80aa05f 100644
--- a/test/e2e/restclient_test.go
+++ b/test/e2e/restclient_test.go
@@ -64,7 +64,6 @@ var (
 	urlContractListeners = "/namespaces/default/contracts/listeners"
 	urlContractAPI       = "/namespaces/default/apis"
 	urlBlockchainEvents  = "/namespaces/default/blockchainevents"
-	urlOperations        = "/namespaces/default/operations"
 	urlGetOrganizations  = "/namespaces/default/network/organizations"
 	urlGetOrgKeys        = "/namespaces/default/identities/%s/verifiers"
 )
@@ -818,14 +817,3 @@ func GetBlockchainEvent(t *testing.T, client *resty.Client, eventID string) (int
 	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
 	return res, err
 }
-
-func GetOperations(t *testing.T, client *resty.Client, startTime time.Time) (operations []*core.Operation) {
-	path := urlOperations
-	resp, err := client.R().
-		SetQueryParam("created", fmt.Sprintf(">%d", startTime.UnixNano())).
-		SetResult(&operations).
-		Get(path)
-	require.NoError(t, err)
-	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
-	return operations
-}
diff --git a/test/e2e/tokens_test.go b/test/e2e/tokens_test.go
index 49113836c..66cd14f58 100644
--- a/test/e2e/tokens_test.go
+++ b/test/e2e/tokens_test.go
@@ -21,7 +21,6 @@ import (
 	"math/rand"
 	"time"
 
-	"github.com/go-resty/resty/v2"
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/stretchr/testify/assert"
@@ -44,10 +43,6 @@ func (suite *TokensTestSuite) BeforeTest(suiteName, testName string) {
 	suite.testState = beforeE2ETest(suite.T())
 }
 
-func (suite *TokensTestSuite) AfterTest(suiteName, testName string) {
-	verifyAllOperationsSucceeded(suite.T(), []*resty.Client{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
-}
-
 func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	defer suite.testState.done()