Repository: coinbase/rosetta-cli
Pull request #120 (2020-08-25 20:10:11)

Integrate Construction API 2.0
Related Issue: #112 
Blocking PR: https://github.com/coinbase/rosetta-sdk-go/pull/118

### Changes
- [x] remove default configuration for `construction` in `configuration`
- [x] integrate `rosetta-sdk-go/constructor`
- [x] add script to validate that example configuration files are correct
- [x] update configuration files (add back Bitcoin in future PR)

coveralls: ## Pull Request Test Coverage Report for [Build 4193](https://coveralls.io/builds/32996893)

* **9** of **15**   **(60.0%)**  changed or added relevant lines in **1** file are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**15.2%**) to **90.173%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [configuration/configuration.go](https://coveralls.io/builds/32996893/source?filename=configuration%2Fconfiguration.go#L422) | 9 | 15 | 60.0%
<!-- | **Total:** | **9** | **15** | **60.0%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [configuration/configuration.go](https://coveralls.io/builds/32996893/source?filename=configuration%2Fconfiguration.go#L434) | 1 | 90.17% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/32996893/badge)](https://coveralls.io/builds/32996893) |
| :-- | --: |
| Change from base [Build 4167](https://coveralls.io/builds/32969879): |  15.2% |
| Covered Lines: | 156 |
| Relevant Lines: | 173 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/Makefile b/Makefile
index 584e184..3b28aaf 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,14 @@ format:
 check-format:
 	! gofmt -s -l . | read;
 
-test:
+validate-configuration-files:
+	go run main.go configuration:validate examples/configuration/ethereum.json;
+	go run main.go configuration:validate examples/configuration/simple.json;
+	go run main.go configuration:create examples/configuration/default.json;
+	go run main.go configuration:validate examples/configuration/default.json;
+	git diff --exit-code;
+
+test: | validate-configuration-files
 	${TEST_SCRIPT}
 
 test-cover:	
diff --git a/cmd/check_construction.go b/cmd/check_construction.go
index 1f1a761..8d02254 100644
--- a/cmd/check_construction.go
+++ b/cmd/check_construction.go
@@ -54,6 +54,10 @@ arbitrary scenarios (i.e. staking, governance).`,
 )
 
 func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
+	if Config.Construction == nil {
+		log.Fatal("construction configuration is missing!")
+	}
+
 	ensureDataDirectoryExists()
 	ctx, cancel := context.WithCancel(context.Background())
 
diff --git a/cmd/utils_train_zstd.go b/cmd/utils_train_zstd.go
index 7ebce92..d1bec9a 100644
--- a/cmd/utils_train_zstd.go
+++ b/cmd/utils_train_zstd.go
@@ -71,7 +71,14 @@ func runTrainZstdCmd(cmd *cobra.Command, args []string) {
 
 	log.Printf("Running zstd training (this could take a while)...")
 
-	_, _, err = storage.BadgerTrain(ctx, namespace, databasePath, dictionaryPath, maxItems, compressorEntries)
+	_, _, err = storage.BadgerTrain(
+		ctx,
+		namespace,
+		databasePath,
+		dictionaryPath,
+		maxItems,
+		compressorEntries,
+	)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/configuration/configuration.go b/configuration/configuration.go
index ef4929c..c26e3ce 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -19,29 +19,15 @@ import (
 	"errors"
 	"fmt"
 	"log"
-	"math/big"
-
-	"github.com/coinbase/rosetta-cli/pkg/scenario"
 
 	"github.com/coinbase/rosetta-sdk-go/asserter"
+	"github.com/coinbase/rosetta-sdk-go/constructor/job"
 	"github.com/coinbase/rosetta-sdk-go/storage"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"github.com/coinbase/rosetta-sdk-go/utils"
 	"github.com/fatih/color"
 )
 
-// AccountingModel is a type representing possible accounting models
-// in the Construction API.
-type AccountingModel string
-
-const (
-	// AccountModel is for account-based blockchains.
-	AccountModel AccountingModel = "account"
-
-	// UtxoModel is for UTXO-based blockchains.
-	UtxoModel AccountingModel = "utxo"
-)
-
 // CheckDataEndCondition is a type of "successful" end
 // for the "check:data" method.
 type CheckDataEndCondition string
@@ -79,19 +65,10 @@ const (
 	DefaultBroadcastLimit                    = 3
 	DefaultTipDelay                          = 300
 	DefaultBlockBroadcastLimit               = 5
-	DefaultNewAccountProbability             = 0.5
-	DefaultMaxAddresses                      = 200
 
 	// ETH Defaults
-	EthereumIDBlockchain    = "Ethereum"
-	EthereumIDNetwork       = "Ropsten"
-	EthereumTransferType    = "transfer"
-	EthereumSymbol          = "ETH"
-	EthereumDecimals        = 18
-	EthereumMinimumBalance  = "0"
-	EthereumMaximumFee      = "5000000000000000" // 0.005 ETH
-	EthereumCurveType       = types.Secp256k1
-	EthereumAccountingModel = AccountModel
+	EthereumIDBlockchain = "Ethereum"
+	EthereumIDNetwork    = "Ropsten"
 )
 
 // Default Configuration Values
@@ -100,41 +77,6 @@ var (
 		Blockchain: EthereumIDBlockchain,
 		Network:    EthereumIDNetwork,
 	}
-	EthereumCurrency = &types.Currency{
-		Symbol:   EthereumSymbol,
-		Decimals: EthereumDecimals,
-	}
-	EthereumTransfer = []*types.Operation{
-		{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 0,
-			},
-			Account: &types.AccountIdentifier{
-				Address: scenario.Sender,
-			},
-			Type: EthereumTransferType,
-			Amount: &types.Amount{
-				Value: scenario.SenderValue,
-			},
-		},
-		{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 1,
-			},
-			RelatedOperations: []*types.OperationIdentifier{
-				{
-					Index: 0,
-				},
-			},
-			Account: &types.AccountIdentifier{
-				Address: scenario.Recipient,
-			},
-			Type: EthereumTransferType,
-			Amount: &types.Amount{
-				Value: scenario.RecipientValue,
-			},
-		},
-	}
 )
 
 // TODO: Add support for sophisticated end conditions
@@ -146,39 +88,6 @@ type ConstructionConfiguration struct {
 	// OfflineURL is the URL of a Rosetta API implementation in "offline mode".
 	OfflineURL string `json:"offline_url"`
 
-	// Currency is the *types.Currency to track and use for transactions.
-	Currency *types.Currency `json:"currency"`
-
-	// MinimumBalance is balance at a particular address
-	// that is not considered spendable.
-	MinimumBalance string `json:"minimum_balance"`
-
-	// MaximumFee is the maximum fee that could be used
-	// to send a transaction. The sendable balance
-	// of any address is calculated as balance - minimum_balance - maximum_fee.
-	MaximumFee string `json:"maximum_fee"`
-
-	// CurveType is the curve to use when generating a *keys.KeyPair.
-	CurveType types.CurveType `json:"curve_type"`
-
-	// AccountingModel is the type of acccount model to use for
-	// testing (account vs UTXO).
-	AccountingModel AccountingModel `json:"accounting_model"`
-
-	// Scenario contains a slice of operations that
-	// indicate how to construct a transaction on a blockchain. In the future
-	// this will be expanded to support all kinds of construction scenarios (like
-	// staking or governance).
-	Scenario []*types.Operation `json:"scenario"`
-
-	// ConfirmationDepth is the number of blocks that must be synced
-	// after a transaction before a transaction is confirmed.
-	//
-	// Note: Rosetta uses Bitcoin's definition of depth, so
-	// a transaction has depth 1 if it is in the head block.
-	// Source: https://en.bitcoin.it/wiki/Confirmation
-	ConfirmationDepth int64 `json:"confirmation_depth"`
-
 	// StaleDepth is the number of blocks to wait before attempting
 	// to rebroadcast after not finding a transaction on-chain.
 	StaleDepth int64 `json:"stale_depth"`
@@ -191,13 +100,6 @@ type ConstructionConfiguration struct {
 	// are broadcast failures (that surpass the BroadcastLimit).
 	IgnoreBroadcastFailures bool `json:"ignore_broadcast_failures"`
 
-	// ChangeScenario is added to the scenario if it is possible to generate
-	// a change transaction where the recipient address is over
-	// the minimum balance. If this is left nil, no change
-	// will ever be created. This is ONLY used for UTXO-based
-	// testing.
-	ChangeScenario *types.Operation `json:"change_scenario"`
-
 	// ClearBroadcasts indicates if all pending broadcasts should
 	// be removed from BroadcastStorage on restart.
 	ClearBroadcasts bool `json:"clear_broadcasts"`
@@ -215,37 +117,14 @@ type ConstructionConfiguration struct {
 	// rebroadcast from BroadcastStorage on restart.
 	RebroadcastAll bool `json:"rebroadcast_all"`
 
-	// NewAccountProbability is the probability we create a new
-	// recipient address on any transaction creation loop.
-	NewAccountProbability float64 `json:"new_account_probability"`
-
-	// MaxAddresses is the maximum number of addresses
-	// to generate while testing.
-	MaxAddresses int `json:"max_addresses"`
-
 	// PrefundedAccounts is an array of prefunded accounts
 	// to use while testing.
 	PrefundedAccounts []*storage.PrefundedAccount `json:"prefunded_accounts"`
-}
 
-// DefaultConstructionConfiguration returns the *ConstructionConfiguration
-// used for testing Ethereum transfers on Ropsten.
-func DefaultConstructionConfiguration() *ConstructionConfiguration {
-	return &ConstructionConfiguration{
-		OfflineURL:            DefaultURL,
-		Currency:              EthereumCurrency,
-		MinimumBalance:        EthereumMinimumBalance,
-		MaximumFee:            EthereumMaximumFee,
-		CurveType:             EthereumCurveType,
-		AccountingModel:       EthereumAccountingModel,
-		Scenario:              EthereumTransfer,
-		ConfirmationDepth:     DefaultConfirmationDepth,
-		StaleDepth:            DefaultStaleDepth,
-		BroadcastLimit:        DefaultBroadcastLimit,
-		BlockBroadcastLimit:   DefaultBlockBroadcastLimit,
-		NewAccountProbability: DefaultNewAccountProbability,
-		MaxAddresses:          DefaultMaxAddresses,
-	}
+	// Workflows are executed by the rosetta-cli to test
+	// certain construction flows. Make sure to define a
+	// "request_funds" and "create_account" workflow.
+	Workflows []*job.Workflow `json:"workflows"`
 }
 
 // DefaultDataConfiguration returns the default *DataConfiguration
@@ -270,7 +149,6 @@ func DefaultConfiguration() *Configuration {
 		SyncConcurrency:        DefaultSyncConcurrency,
 		TransactionConcurrency: DefaultTransactionConcurrency,
 		TipDelay:               DefaultTipDelay,
-		Construction:           DefaultConstructionConfiguration(),
 		Data:                   DefaultDataConfiguration(),
 	}
 }
@@ -433,41 +311,13 @@ func populateConstructionMissingFields(
 	constructionConfig *ConstructionConfiguration,
 ) *ConstructionConfiguration {
 	if constructionConfig == nil {
-		return DefaultConstructionConfiguration()
+		return nil
 	}
 
 	if len(constructionConfig.OfflineURL) == 0 {
 		constructionConfig.OfflineURL = DefaultURL
 	}
 
-	if constructionConfig.Currency == nil {
-		constructionConfig.Currency = EthereumCurrency
-	}
-
-	if len(constructionConfig.MinimumBalance) == 0 {
-		constructionConfig.MinimumBalance = EthereumMinimumBalance
-	}
-
-	if len(constructionConfig.MaximumFee) == 0 {
-		constructionConfig.MaximumFee = EthereumMaximumFee
-	}
-
-	if len(constructionConfig.CurveType) == 0 {
-		constructionConfig.CurveType = EthereumCurveType
-	}
-
-	if len(constructionConfig.AccountingModel) == 0 {
-		constructionConfig.AccountingModel = EthereumAccountingModel
-	}
-
-	if len(constructionConfig.Scenario) == 0 {
-		constructionConfig.Scenario = EthereumTransfer
-	}
-
-	if constructionConfig.ConfirmationDepth == 0 {
-		constructionConfig.ConfirmationDepth = DefaultConfirmationDepth
-	}
-
 	if constructionConfig.StaleDepth == 0 {
 		constructionConfig.StaleDepth = DefaultStaleDepth
 	}
@@ -480,14 +330,6 @@ func populateConstructionMissingFields(
 		constructionConfig.BlockBroadcastLimit = DefaultBlockBroadcastLimit
 	}
 
-	if constructionConfig.NewAccountProbability == 0 {
-		constructionConfig.NewAccountProbability = DefaultNewAccountProbability
-	}
-
-	if constructionConfig.MaxAddresses == 0 {
-		constructionConfig.MaxAddresses = DefaultMaxAddresses
-	}
-
 	return constructionConfig
 }
 
@@ -550,59 +392,64 @@ func populateMissingFields(config *Configuration) *Configuration {
 	return config
 }
 
-func checkStringUint(input string) error {
-	val, ok := new(big.Int).SetString(input, 10)
-	if !ok {
-		return fmt.Errorf("%s is not an integer", input)
-	}
-
-	if val.Sign() == -1 {
-		return fmt.Errorf("%s must not be negative", input)
-	}
-
-	return nil
-}
-
 func assertConstructionConfiguration(config *ConstructionConfiguration) error {
-	// TODO: add asserter.Currency method
-	if err := asserter.Amount(&types.Amount{Value: "0", Currency: config.Currency}); err != nil {
-		return fmt.Errorf("%w: invalid currency", err)
+	if config == nil {
+		return nil
 	}
 
-	switch config.AccountingModel {
-	case AccountModel, UtxoModel:
-	default:
-		return fmt.Errorf("accounting model %s not supported", config.AccountingModel)
-	}
+	seenCreateAccount := false
+	seenRequestFunds := false
+	for _, workflow := range config.Workflows {
+		sawReserved := false
+		if workflow.Name == string(job.CreateAccount) {
+			sawReserved = true
+			seenCreateAccount = true
+		}
 
-	if err := asserter.CurveType(config.CurveType); err != nil {
-		return fmt.Errorf("%w: invalid curve type", err)
+		if workflow.Name == string(job.RequestFunds) {
+			seenRequestFunds = true
+			sawReserved = true
+		}
+
+		if sawReserved && workflow.Concurrency != job.ReservedWorkflowConcurrency {
+			return errors.New("reserved workflow must have concurrency 1")
+		}
 	}
 
-	if err := checkStringUint(config.MinimumBalance); err != nil {
-		return fmt.Errorf("%w: invalid value for MinimumBalance", err)
+	if !seenCreateAccount {
+		return errors.New("missing create_account workflow")
 	}
 
-	if err := checkStringUint(config.MaximumFee); err != nil {
-		return fmt.Errorf("%w: invalid value for MaximumFee", err)
+	if !seenRequestFunds {
+		return errors.New("missing request_funds workflow")
 	}
 
 	for _, account := range config.PrefundedAccounts {
 		// Checks that privkey is hex encoded
 		_, err := hex.DecodeString(account.PrivateKeyHex)
 		if err != nil {
-			return fmt.Errorf("%s is not hex encoded", account.PrivateKeyHex)
+			return fmt.Errorf(
+				"%w: private key %s is not hex encoded for prefunded account",
+				err,
+				account.PrivateKeyHex,
+			)
 		}
 
 		// Checks if valid curvetype
 		err = asserter.CurveType(account.CurveType)
 		if err != nil {
-			return fmt.Errorf("invalid CurveType %s", err)
+			return fmt.Errorf("%w: invalid CurveType for prefunded account", err)
 		}
 
 		// Checks if address is not empty string
 		if account.Address == "" {
-			return fmt.Errorf("Account.Address is missing")
+			return fmt.Errorf("Account.Address is missing for prefunded account")
+		}
+
+		// Check if currency is valid
+		err = asserter.Amount(&types.Amount{Value: "0", Currency: account.Currency})
+		if err != nil {
+			return fmt.Errorf("%w: invalid currency for prefunded account", err)
 		}
 	}
 
diff --git a/examples/configuration/bitcoin.json b/examples/configuration/bitcoin.json
deleted file mode 100644
index ea1645b..0000000
--- a/examples/configuration/bitcoin.json
+++ /dev/null
@@ -1,71 +0,0 @@
-{
- "network": {
-  "blockchain": "Bitcoin",
-  "network": "Testnet3"
- },
- "data_directory": "bitcoin-data",
- "http_timeout": 300,
- "tip_delay": 1800,
- "construction": {
-  "currency": {
-   "symbol": "BTC",
-   "decimals": 8
-  },
-  "minimum_balance": "600",
-  "maximum_fee": "500",
-  "curve_type": "secp256k1",
-  "accounting_model": "utxo",
-  "scenario": [
-   {
-    "operation_identifier": {
-     "index": 0
-    },
-    "type": "Vin",
-    "status": "",
-    "account": {
-     "address": "{{ SENDER }}"
-    },
-    "amount": {
-     "value": "{{ SENDER_VALUE }}",
-     "currency": null
-    },
-    "coin_change": {
-      "coin_action": "coin_spent",
-      "coin_identifier": {
-        "identifier": "{{ COIN_IDENTIFIER }}"
-      }
-    }
-   },
-   {
-    "operation_identifier": {
-     "index": 1
-    },
-    "type": "Vout",
-    "status": "",
-    "account": {
-     "address": "{{ RECIPIENT }}"
-    },
-    "amount": {
-     "value": "{{ RECIPIENT_VALUE }}",
-     "currency": null
-    }
-   }
-  ],
-  "confirmation_depth": 1,
-  "stale_depth": 1,
-  "change_scenario": {
-    "operation_identifier": {
-     "index": 2
-    },
-    "type": "Vout",
-    "status": "",
-    "account": {
-     "address": "{{ CHANGE_ADDRESS }}"
-    },
-    "amount": {
-     "value": "{{ CHANGE_VALUE }}",
-     "currency": null
-    }
-  }
- }
-}
diff --git a/examples/configuration/default.json b/examples/configuration/default.json
index 7959e63..530cd1f 100644
--- a/examples/configuration/default.json
+++ b/examples/configuration/default.json
@@ -6,68 +6,13 @@
  "online_url": "http://localhost:8080",
  "data_directory": "",
  "http_timeout": 10,
+ "retry_elapsed_time": 60,
  "sync_concurrency": 8,
  "transaction_concurrency": 16,
  "tip_delay": 300,
  "disable_memory_limit": false,
  "log_configuration": false,
- "construction": {
-  "offline_url": "http://localhost:8080",
-  "currency": {
-   "symbol": "ETH",
-   "decimals": 18
-  },
-  "minimum_balance": "0",
-  "maximum_fee": "5000000000000000",
-  "curve_type": "secp256k1",
-  "accounting_model": "account",
-  "scenario": [
-   {
-    "operation_identifier": {
-     "index": 0
-    },
-    "type": "transfer",
-    "status": "",
-    "account": {
-     "address": "{{ SENDER }}"
-    },
-    "amount": {
-     "value": "{{ SENDER_VALUE }}",
-     "currency": null
-    }
-   },
-   {
-    "operation_identifier": {
-     "index": 1
-    },
-    "related_operations": [
-     {
-      "index": 0
-     }
-    ],
-    "type": "transfer",
-    "status": "",
-    "account": {
-     "address": "{{ RECIPIENT }}"
-    },
-    "amount": {
-     "value": "{{ RECIPIENT_VALUE }}",
-     "currency": null
-    }
-   }
-  ],
-  "confirmation_depth": 10,
-  "stale_depth": 30,
-  "broadcast_limit": 3,
-  "ignore_broadcast_failures": false,
-  "change_scenario": null,
-  "clear_broadcasts": false,
-  "broadcast_behind_tip": false,
-  "block_broadcast_limit": 5,
-  "rebroadcast_all": false,
-  "new_account_probability": 0.5,
-  "max_addresses": 200
- },
+ "construction": null,
  "data": {
   "active_reconciliation_concurrency": 16,
   "inactive_reconciliation_concurrency": 4,
@@ -84,6 +29,7 @@
   "reconciliation_disabled": false,
   "inactive_discrepency_search_disabled": false,
   "balance_tracking_disabled": false,
-  "coin_tracking_disabled": false
+  "coin_tracking_disabled": false,
+  "results_output_file": ""
  }
 }
\ No newline at end of file
diff --git a/examples/configuration/ethereum.json b/examples/configuration/ethereum.json
index d42d7ef..d6423bc 100644
--- a/examples/configuration/ethereum.json
+++ b/examples/configuration/ethereum.json
@@ -3,52 +3,153 @@
   "blockchain": "Ethereum",
   "network": "Ropsten"
  },
- "data_directory": "eth-data",
+ "online_url": "",
+ "data_directory": "",
  "http_timeout": 300,
+ "retry_elapsed_time": 0,
+ "sync_concurrency": 0,
+ "transaction_concurrency": 0,
  "tip_delay": 60,
+ "disable_memory_limit": false,
+ "log_configuration": false,
  "construction": {
-  "currency": {
-   "symbol": "ETH",
-   "decimals": 18
-  },
-  "minimum_balance": "0",
-  "maximum_fee": "5000000000000000",
-  "curve_type": "secp256k1",
-  "accounting_model": "account",
-  "scenario": [
+  "offline_url": "",
+  "stale_depth": 0,
+  "broadcast_limit": 0,
+  "ignore_broadcast_failures": false,
+  "clear_broadcasts": false,
+  "broadcast_behind_tip": false,
+  "block_broadcast_limit": 0,
+  "rebroadcast_all": false,
+  "prefunded_accounts": null,
+  "workflows": [
    {
-    "operation_identifier": {
-     "index": 0
-    },
-    "type": "transfer",
-    "status": "",
-    "account": {
-     "address": "{{ SENDER }}"
-    },
-    "amount": {
-     "value": "{{ SENDER_VALUE }}",
-     "currency": null
-    }
+    "name": "request_funds",
+    "concurrency": 1,
+    "scenarios": [
+     {
+      "name": "find_address",
+      "actions": [
+       {
+        "input": "{\"symbol\":\"ETH\", \"decimals\":18}",
+        "type": "set_variable",
+        "output_path": "currency"
+       },
+       {
+        "input": "{\"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\":1}",
+        "type": "find_balance",
+        "output_path": "random_address"
+       }
+      ]
+     },
+     {
+      "name": "request",
+      "actions": [
+       {
+        "input": "{\"address\": {{random_address.account.address}}, \"minimum_balance\":{\"value\": \"10000000000000000\", \"currency\": {{currency}}}}",
+        "type": "find_balance",
+        "output_path": "loaded_address"
+       }
+      ]
+     }
+    ]
+   },
+   {
+    "name": "create_account",
+    "concurrency": 1,
+    "scenarios": [
+     {
+      "name": "create_account",
+      "actions": [
+       {
+        "input": "{\"network\":\"Ropsten\", \"blockchain\":\"Ethereum\"}",
+        "type": "set_variable",
+        "output_path": "network"
+       },
+       {
+        "input": "{\"curve_type\": \"secp256k1\"}",
+        "type": "generate_key",
+        "output_path": "key"
+       },
+       {
+        "input": "{\"network_identifier\": {{network}}, \"public_key\": {{key.public_key}}}",
+        "type": "derive",
+        "output_path": "address"
+       },
+       {
+        "input": "{\"address\": {{address.address}}, \"keypair\": {{key}}}",
+        "type": "save_address"
+       }
+      ]
+     }
+    ]
    },
    {
-    "operation_identifier": {
-     "index": 1
-    },
-    "related_operations": [
+    "name": "transfer",
+    "concurrency": 10,
+    "scenarios": [
      {
-      "index": 0
+      "name": "transfer",
+      "actions": [
+       {
+        "input": "{\"network\":\"Ropsten\", \"blockchain\":\"Ethereum\"}",
+        "type": "set_variable",
+        "output_path": "transfer.network"
+       },
+       {
+        "input": "{\"symbol\":\"ETH\", \"decimals\":18}",
+        "type": "set_variable",
+        "output_path": "currency"
+       },
+       {
+        "input": "{\"minimum_balance\":{\"value\": \"10000000000000000\", \"currency\": {{currency}}}}",
+        "type": "find_balance",
+        "output_path": "sender"
+       },
+       {
+        "input": "\"42000000000000\"",
+        "type": "set_variable",
+        "output_path": "max_fee"
+       },
+       {
+        "input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{max_fee}}}",
+        "type": "math",
+        "output_path": "available_amount"
+       },
+       {
+        "input": "{\"minimum\": \"1\", \"maximum\": {{available_amount}}}",
+        "type": "random_number",
+        "output_path": "recipient_amount"
+       },
+       {
+        "input": "{\"recipient_amount\":{{recipient_amount}}}",
+        "type": "print_message"
+       },
+       {
+        "input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{recipient_amount}}}",
+        "type": "math",
+        "output_path": "sender_amount"
+       },
+       {
+        "input": "{\"not_address\":[{{sender.account.address}}], \"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\": 100, \"create_probability\": 50}",
+        "type": "find_balance",
+        "output_path": "recipient"
+       },
+       {
+        "input": "\"1\"",
+        "type": "set_variable",
+        "output_path": "transfer.confirmation_depth"
+       },
+       {
+        "input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"transfer\",\"account\":{\"address\":{{sender.account.address}}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"transfer\",\"account\":{\"address\":{{recipient.account.address}}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
+        "type": "set_variable",
+        "output_path": "transfer.operations"
+       }
+      ]
      }
-    ],
-    "type": "transfer",
-    "status": "",
-    "account": {
-     "address": "{{ RECIPIENT }}"
-    },
-    "amount": {
-     "value": "{{ RECIPIENT_VALUE }}",
-     "currency": null
-    }
+    ]
    }
   ]
- }
-}
+ },
+ "data": null
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 7939317..f7f7190 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/coinbase/rosetta-cli
 go 1.13
 
 require (
-	github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200824221853-d7b1fe2f9239
+	github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200901005342-2c16b7a9ff30
 	github.com/fatih/color v1.9.0
 	github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
 	github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c
diff --git a/go.sum b/go.sum
index a9a2b3d..9a5320e 100644
--- a/go.sum
+++ b/go.sum
@@ -59,6 +59,56 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
 github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200824221853-d7b1fe2f9239 h1:eP88DoIkcbMG5zHja3lUh4F2c+9am1T0zk/AChy1Vpo=
 github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200824221853-d7b1fe2f9239/go.mod h1:/dKD5dZJ0bzDXrd/M3XMQmpXRk0bru3s9EMIeyBJfy4=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825180137-4b6b8c12e59e h1:gPomoE/+wkTz6/eFWdugcOvWlH/FjiKjNgsIrHrrhZ0=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825180137-4b6b8c12e59e/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825192950-ca4a1cbb4439 h1:G6A5/O67JrbIKEJt/F3vbGWSiQjeUC0mHJFAnP40VtA=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825192950-ca4a1cbb4439/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825193453-990ba41985fe h1:HKdKRwXIYzQqxf8hss365GMrYeKEBlVNe7aR8s15cfw=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825193453-990ba41985fe/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825200012-1a2b012c535a h1:iVHj3zPYATksd7snj3zcuvuozEBpz6QmWXVxVgyAofY=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825200012-1a2b012c535a/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825202822-ec2ccf5ca7b2 h1:czjlrSF+F+ZrZF38hPOqk7NcVCg9j7ITzbaWwNXcRYY=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825202822-ec2ccf5ca7b2/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825203604-1e6ee29e1d18 h1:qGK7Gsn5TdVVyPGM3gTUhJQcVWKWcVTK7vOxMvqWs1I=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825203604-1e6ee29e1d18/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825215532-31a92f0c1fb1 h1:Gc4zbvMijWKo9Wh8T1LnDDesViA2XK81UpA0vrfPisQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825215532-31a92f0c1fb1/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220011-d26cae449af1 h1:hBBzJARSy/TohObIr4SYnFWBYCDWg2rI5szOlmKO3CE=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220011-d26cae449af1/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220704-401164ea449f h1:g30vKRyRFXzTTjM/+JldTv3oCyRN/fsqfK4G9itv4VY=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220704-401164ea449f/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220938-e8c8639f9de9 h1:sp4HYBk/hBx84ANf9a6/hlHxkhko9GvgBDUkjyQH2xI=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825220938-e8c8639f9de9/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825221259-01f4635468d6 h1:Sq7CNu6InK1Sa4mChRekU284Dw2FcSG8+Q3Ab8b/OAI=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825221259-01f4635468d6/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825222405-5a0a89fadb36 h1:rct5jMSTTzxZuzTps1NxmnjoS4abr5fHch6Dql+yckw=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825222405-5a0a89fadb36/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825232904-4639051d5690 h1:vVuAzyf8NtTE8uiUm7ypVdJlL+RLtUS2Tf9prMAQVFc=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200825232904-4639051d5690/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826005614-8eaafea66bf9 h1:PlhYUghjjT8RqyTtzfGdIoL4F7S50qK9m41RnpG2NlA=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826005614-8eaafea66bf9/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826011303-e311283c1503 h1:rrBbzCG8CV1CzkGmSMufzadPiXIQg05jGRaglK/UKxY=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826011303-e311283c1503/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826014430-cb55ba6945b5 h1:oSq8L9omheiEsNP/VBwrMsSfeggtvnsywL+c5QIcM6Y=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826014430-cb55ba6945b5/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826042726-a9d02cfb0622 h1:3N+w0kT3GfVWFdvs0o+2DjP/WvX7C8RW3M7GtJuTf0Q=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826042726-a9d02cfb0622/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826043835-9595c3a2c361 h1:puy1CPZtv3DPkNF8kCKaj6WdYYh4f2Pn9FLlCqKxcq0=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826043835-9595c3a2c361/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826044804-4e9a062a840a h1:D5GhIG17ChlUYi5s4AECsp+o7GgFhEHQ6AZXBlMiDrk=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826044804-4e9a062a840a/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826163301-f4e18c63d4d4 h1:9ragTKRH6HZitJ5zTA0dIoyP30kLQ/uyj65Ky6QYo4I=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826163301-f4e18c63d4d4/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826173843-212f2d853cdc h1:VHW//kbg1wfvHz/1wOvPBiFh+ljbAYT+dXbZcuPjNAY=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200826173843-212f2d853cdc/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831181253-2c90cada22a0 h1:/IuP9Dle50p8d1/sN/A+zIMJ61j1hmc25lG9CzDb1Qw=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831181253-2c90cada22a0/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831182036-449adc1bbece h1:BOSDN75he/HVJvCJzH2svBHXUTX+9v0X93bbRnduUeU=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831182036-449adc1bbece/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831200955-3c1868507a81 h1:6R/X/QdYk8mdIrMo6bqC5RuaNbdmpJWRCWhrDl5db9Q=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200831200955-3c1868507a81/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200901005342-2c16b7a9ff30 h1:beBdN7OZ0OO/wjADaievjIE0OAuk0qb4zBd2wNn9NfQ=
+github.com/coinbase/rosetta-sdk-go v0.3.5-0.20200901005342-2c16b7a9ff30/go.mod h1:ERvHFmf3+JUQB/6ZysASs8CCEcWJMAhN+y/ioqnBsLQ=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -81,7 +131,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
 github.com/dgraph-io/badger/v2 v2.0.3 h1:inzdf6VF/NZ+tJ8RwwYMjJMvsOALTHYdozn0qSl6XJI=
 github.com/dgraph-io/badger/v2 v2.0.3/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM=
+github.com/dgraph-io/badger/v2 v2.2007.1 h1:t36VcBCpo4SsmAD5M8wVv1ieVzcALyGfaJ92z4ccULM=
+github.com/dgraph-io/badger/v2 v2.2007.1/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
 github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
 github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -103,6 +156,7 @@ github.com/ethereum/go-ethereum v1.9.18/go.mod h1:JSSTypSMTkGZtAdAChH2wP5dZEvPGh
 github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -298,10 +352,14 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
 github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
 github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
 github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
 github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
diff --git a/pkg/constructor/constructor.go b/pkg/constructor/constructor.go
deleted file mode 100644
index 0e08ba7..0000000
--- a/pkg/constructor/constructor.go
+++ /dev/null
@@ -1,998 +0,0 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// 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 constructor
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"log"
-	"math/big"
-	"time"
-
-	"github.com/coinbase/rosetta-cli/configuration"
-	"github.com/coinbase/rosetta-cli/pkg/logger"
-	"github.com/coinbase/rosetta-cli/pkg/scenario"
-
-	"github.com/coinbase/rosetta-sdk-go/keys"
-	"github.com/coinbase/rosetta-sdk-go/parser"
-	"github.com/coinbase/rosetta-sdk-go/storage"
-	"github.com/coinbase/rosetta-sdk-go/types"
-	"github.com/coinbase/rosetta-sdk-go/utils"
-	"github.com/fatih/color"
-	"github.com/jinzhu/copier"
-)
-
-// action is some supported intent we can
-// perform. This is used to determine the minimum
-// required balance to complete the transaction.
-type action string
-
-const (
-	convertToInt = 100
-
-	// defaultSleepTime is the default time we sleep
-	// while waiting to perform the next task.
-	defaultSleepTime = 10
-
-	// newAccountSend is a send to a new account.
-	newAccountSend action = "new-account-send"
-
-	// ExistingAccountSend is a send to an existing account.
-	existingAccountSend action = "existing-account-send"
-
-	// changeSend is a send that creates a UTXO
-	// for the recipient and sends the remainder
-	// to a change UTXO.
-	changeSend action = "change-send"
-
-	// fullSend is a send that transfers
-	// all value in one UTXO into another.
-	fullSend action = "full-send"
-)
-
-var (
-	// ErrInsufficientFunds is returned when we must
-	// request funds.
-	ErrInsufficientFunds = errors.New("insufficient funds")
-)
-
-// Helper is an interface that provides access
-// to information needed by the constructor while
-// creating transactions.
-type Helper interface {
-	// Derive returns a new address for a provided publicKey.
-	Derive(
-		context.Context,
-		*types.NetworkIdentifier,
-		*types.PublicKey,
-		map[string]interface{},
-	) (string, map[string]interface{}, error)
-
-	// Preprocess calls the /construction/preprocess endpoint
-	// on an offline node.
-	Preprocess(
-		context.Context,
-		*types.NetworkIdentifier,
-		[]*types.Operation,
-		map[string]interface{},
-	) (map[string]interface{}, error)
-
-	// Metadata calls the /construction/metadata endpoint
-	// using the online node.
-	Metadata(
-		context.Context,
-		*types.NetworkIdentifier,
-		map[string]interface{},
-	) (map[string]interface{}, error)
-
-	// Payloads calls the /construction/payloads endpoint
-	// using the offline node.
-	Payloads(
-		context.Context,
-		*types.NetworkIdentifier,
-		[]*types.Operation,
-		map[string]interface{},
-	) (string, []*types.SigningPayload, error)
-
-	// Parse calls the /construction/parse endpoint
-	// using the offline node.
-	Parse(
-		context.Context,
-		*types.NetworkIdentifier,
-		bool, // signed
-		string, // transaction
-	) ([]*types.Operation, []string, map[string]interface{}, error)
-
-	// Combine calls the /construction/combine endpoint
-	// using the offline node.
-	Combine(
-		context.Context,
-		*types.NetworkIdentifier,
-		string, // unsigned transaction
-		[]*types.Signature,
-	) (string, error)
-
-	// Broadcast enqueues a particular intent for broadcast.
-	Broadcast(
-		context.Context,
-		string, // sender
-		[]*types.Operation, // intent
-		*types.TransactionIdentifier,
-		string, // payload
-	) error
-
-	// Hash calls the /construction/hash endpoint
-	// using the offline node.
-	Hash(
-		context.Context,
-		*types.NetworkIdentifier,
-		string, // network transaction
-	) (*types.TransactionIdentifier, error)
-
-	// Sign returns signatures for the provided
-	// payloads.
-	Sign(
-		context.Context,
-		[]*types.SigningPayload,
-	) ([]*types.Signature, error)
-
-	// StoreKey is called to persist an
-	// address + KeyPair.
-	StoreKey(
-		context.Context,
-		string,
-		*keys.KeyPair,
-	) error
-
-	// AccountBalance returns the balance
-	// for a provided address.
-	AccountBalance(
-		context.Context,
-		*types.AccountIdentifier,
-		*types.Currency,
-	) (*big.Int, error)
-
-	// CoinBalance returns the balance of the largest
-	// Coin owned by an address.
-	CoinBalance(
-		context.Context,
-		*types.AccountIdentifier,
-		*types.Currency,
-	) (*big.Int, *types.CoinIdentifier, error)
-
-	// LockedAddresses is a slice of all addresses currently sending or receiving
-	// funds.
-	LockedAddresses(context.Context) ([]string, error)
-
-	// AllBroadcasts returns a slice of all in-progress broadcasts.
-	AllBroadcasts(ctx context.Context) ([]*storage.Broadcast, error)
-
-	// ClearBroadcasts deletes all pending broadcasts.
-	ClearBroadcasts(ctx context.Context) ([]*storage.Broadcast, error)
-
-	// AllAddresses returns a slice of all known addresses.
-	AllAddresses(ctx context.Context) ([]string, error)
-
-	// RandomAmount returns some integer between min and max.
-	RandomAmount(*big.Int, *big.Int) *big.Int
-
-	// HeadBlockExists returns a boolean indicating if a block
-	// has been synced by BlockStorage.
-	HeadBlockExists(context.Context) bool
-}
-
-// Handler is an interface called by the constructor whenever
-// an address is created or a transaction is created.
-type Handler interface {
-	AddressCreated(context.Context, string) error
-	TransactionCreated(context.Context, string, *types.TransactionIdentifier) error
-}
-
-// Constructor is responsible for managing the entire flow
-// of creating test transactions on the Construction API.
-// This runs from creating keypairs to requesting funds be
-// loaded on addresses to creating transactions that satisfy
-// minimum balance constraints.
-type Constructor struct {
-	network               *types.NetworkIdentifier
-	accountingModel       configuration.AccountingModel
-	currency              *types.Currency
-	minimumBalance        *big.Int
-	maximumFee            *big.Int
-	curveType             types.CurveType
-	newAccountProbability float64
-	maxAddresses          int
-
-	scenario       []*types.Operation
-	changeScenario *types.Operation
-
-	parser  *parser.Parser
-	helper  Helper
-	handler Handler
-}
-
-// New returns a new *Constructor.
-func New(
-	config *configuration.Configuration,
-	parser *parser.Parser,
-	helper Helper,
-	handler Handler,
-) (*Constructor, error) {
-	minimumBalance, ok := new(big.Int).SetString(config.Construction.MinimumBalance, 10)
-	if !ok {
-		return nil, errors.New("cannot parse minimum balance")
-	}
-
-	maximumFee, ok := new(big.Int).SetString(config.Construction.MaximumFee, 10)
-	if !ok {
-		return nil, errors.New("cannot parse maximum fee")
-	}
-
-	return &Constructor{
-		network:               config.Network,
-		accountingModel:       config.Construction.AccountingModel,
-		currency:              config.Construction.Currency,
-		minimumBalance:        minimumBalance,
-		maximumFee:            maximumFee,
-		curveType:             config.Construction.CurveType,
-		newAccountProbability: config.Construction.NewAccountProbability,
-		maxAddresses:          config.Construction.MaxAddresses,
-		scenario:              config.Construction.Scenario,
-		changeScenario:        config.Construction.ChangeScenario,
-		helper:                helper,
-		handler:               handler,
-	}, nil
-}
-
-// createTransaction constructs and signs a transaction with the provided intent.
-func (c *Constructor) createTransaction(
-	ctx context.Context,
-	intent []*types.Operation,
-) (*types.TransactionIdentifier, string, error) {
-	metadataRequest, err := c.helper.Preprocess(
-		ctx,
-		c.network,
-		intent,
-		nil,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to preprocess", err)
-	}
-
-	requiredMetadata, err := c.helper.Metadata(
-		ctx,
-		c.network,
-		metadataRequest,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to construct metadata", err)
-	}
-
-	unsignedTransaction, payloads, err := c.helper.Payloads(
-		ctx,
-		c.network,
-		intent,
-		requiredMetadata,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to construct payloads", err)
-	}
-
-	parsedOps, signers, _, err := c.helper.Parse(
-		ctx,
-		c.network,
-		false,
-		unsignedTransaction,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to parse unsigned transaction", err)
-	}
-
-	if len(signers) != 0 {
-		return nil, "", fmt.Errorf(
-			"signers should be empty in unsigned transaction but found %d",
-			len(signers),
-		)
-	}
-
-	if err := c.parser.ExpectedOperations(intent, parsedOps, false, false); err != nil {
-		return nil, "", fmt.Errorf("%w: unsigned parsed ops do not match intent", err)
-	}
-
-	signatures, err := c.helper.Sign(ctx, payloads)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to sign payloads", err)
-	}
-
-	networkTransaction, err := c.helper.Combine(
-		ctx,
-		c.network,
-		unsignedTransaction,
-		signatures,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to combine signatures", err)
-	}
-
-	signedParsedOps, signers, _, err := c.helper.Parse(
-		ctx,
-		c.network,
-		true,
-		networkTransaction,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to parse signed transaction", err)
-	}
-
-	if err := c.parser.ExpectedOperations(intent, signedParsedOps, false, false); err != nil {
-		return nil, "", fmt.Errorf("%w: signed parsed ops do not match intent", err)
-	}
-
-	if err := parser.ExpectedSigners(payloads, signers); err != nil {
-		return nil, "", fmt.Errorf("%w: signed transactions signers do not match intent", err)
-	}
-
-	transactionIdentifier, err := c.helper.Hash(
-		ctx,
-		c.network,
-		networkTransaction,
-	)
-	if err != nil {
-		return nil, "", fmt.Errorf("%w: unable to get transaction hash", err)
-	}
-
-	return transactionIdentifier, networkTransaction, nil
-}
-
-// newAddress generates a new keypair and
-// derives its address offline. This only works
-// for blockchains that don't require an on-chain
-// action to create an account.
-func (c *Constructor) newAddress(ctx context.Context) (string, error) {
-	kp, err := keys.GenerateKeypair(c.curveType)
-	if err != nil {
-		return "", fmt.Errorf("%w unable to generate keypair", err)
-	}
-
-	address, _, err := c.helper.Derive(
-		ctx,
-		c.network,
-		kp.PublicKey,
-		nil,
-	)
-
-	if err != nil {
-		return "", fmt.Errorf("%w: unable to derive address", err)
-	}
-
-	err = c.helper.StoreKey(ctx, address, kp)
-	if err != nil {
-		return "", fmt.Errorf("%w: unable to store address", err)
-	}
-
-	if err := c.handler.AddressCreated(ctx, address); err != nil {
-		return "", fmt.Errorf("%w: could not handle address creation", err)
-	}
-
-	return address, nil
-}
-
-// requestFunds prompts the user to load
-// a particular address with funds from a faucet.
-// TODO: automate this using an API faucet.
-func (c *Constructor) requestFunds(
-	ctx context.Context,
-	address string,
-) (*big.Int, *types.CoinIdentifier, error) {
-	var lastMessage string
-	for ctx.Err() == nil {
-		balance, coinIdentifier, err := c.balance(ctx, address)
-		if err != nil {
-			return nil, nil, err
-		}
-
-		minBalance := c.minimumRequiredBalance(newAccountSend)
-		if c.accountingModel == configuration.UtxoModel {
-			minBalance = c.minimumRequiredBalance(changeSend)
-		}
-
-		if balance != nil && new(big.Int).Sub(balance, minBalance).Sign() != -1 {
-			color.Green("Found balance %s on %s", utils.PrettyAmount(balance, c.currency), address)
-			return balance, coinIdentifier, nil
-		}
-
-		message := fmt.Sprintf("Waiting for funds on %s", address)
-		if balance != nil && balance.Sign() == 1 {
-			message = fmt.Sprintf(
-				"Found balance %s on %s (need %s to continue)",
-				utils.PrettyAmount(balance, c.currency),
-				address,
-				utils.PrettyAmount(minBalance, c.currency),
-			)
-		}
-
-		if message != lastMessage {
-			color.Yellow(message)
-			lastMessage = message
-		}
-
-		time.Sleep(defaultSleepTime * time.Second)
-	}
-
-	return nil, nil, ctx.Err()
-}
-
-func (c *Constructor) minimumRequiredBalance(action action) *big.Int {
-	doubleMinimumBalance := new(big.Int).Add(c.minimumBalance, c.minimumBalance)
-	switch action {
-	case newAccountSend, changeSend:
-		// In this account case, we must have keep a balance above
-		// the minimum_balance in the sender's account and send
-		// an amount of at least the minimum_balance to the recipient.
-		//
-		// In the UTXO case, we must send at least the minimum
-		// balance to the recipient and the change address (or
-		// we will create dust).
-		return new(big.Int).Add(doubleMinimumBalance, c.maximumFee)
-	case existingAccountSend, fullSend:
-		// In the account case, we must keep a balance above
-		// the minimum_balance in the sender's account.
-		//
-		// In the UTXO case, we must send at least the minimum
-		// balance to the new UTXO.
-		return new(big.Int).Add(c.minimumBalance, c.maximumFee)
-	}
-
-	return nil
-}
-
-// balance returns the total balance to use for
-// a transfer. In the case of a UTXO-based chain,
-// this is the largest remaining UTXO.
-func (c *Constructor) balance(
-	ctx context.Context,
-	address string,
-) (*big.Int, *types.CoinIdentifier, error) {
-	accountIdentifier := &types.AccountIdentifier{Address: address}
-
-	switch c.accountingModel {
-	case configuration.AccountModel:
-		bal, err := c.helper.AccountBalance(ctx, accountIdentifier, c.currency)
-
-		return bal, nil, err
-	case configuration.UtxoModel:
-		return c.helper.CoinBalance(ctx, accountIdentifier, c.currency)
-	}
-
-	return nil, nil, fmt.Errorf("unable to find balance for %s", address)
-}
-
-func (c *Constructor) bestUnlockedSender(
-	ctx context.Context,
-	addresses []string,
-) (
-	string, // best address
-	*big.Int, // best balance
-	*types.CoinIdentifier, // best coin
-	error,
-) {
-	unlockedAddresses := []string{}
-	lockedAddresses, err := c.helper.LockedAddresses(ctx)
-	if err != nil {
-		return "", nil, nil, fmt.Errorf("%w: unable to get locked addresses", err)
-	}
-
-	// Convert to a map so can do fast lookups
-	lockedSet := map[string]struct{}{}
-	for _, address := range lockedAddresses {
-		lockedSet[address] = struct{}{}
-	}
-
-	for _, address := range addresses {
-		if _, exists := lockedSet[address]; !exists {
-			unlockedAddresses = append(unlockedAddresses, address)
-		}
-	}
-
-	// Only check addresses not currently locked
-	var bestAddress string
-	var bestBalance *big.Int
-	var bestCoin *types.CoinIdentifier
-	for _, address := range unlockedAddresses {
-		balance, coinIdentifier, err := c.balance(ctx, address)
-		if err != nil {
-			return "", nil, nil, fmt.Errorf("%w: unable to get balance for %s", err, address)
-		}
-
-		if bestBalance == nil || new(big.Int).Sub(bestBalance, balance).Sign() == -1 {
-			bestAddress = address
-			bestBalance = balance
-			bestCoin = coinIdentifier
-		}
-	}
-
-	return bestAddress, bestBalance, bestCoin, nil
-}
-
-// findSender fetches all available addresses,
-// all locked addresses, and all address balances
-// to determine which addresses can facilitate
-// a transfer. The sender with the highest
-// balance is returned (or the largest UTXO).
-func (c *Constructor) findSender(
-	ctx context.Context,
-) (
-	string, // sender
-	*big.Int, // balance
-	*types.CoinIdentifier, // coin
-	error,
-) {
-	for ctx.Err() == nil {
-		addresses, err := c.helper.AllAddresses(ctx)
-		if err != nil {
-			return "", nil, nil, fmt.Errorf("%w: unable to get addresses", err)
-		}
-
-		if len(addresses) == 0 { // create new and load
-			err := c.generateNewAndRequest(ctx)
-			if err != nil {
-				return "", nil, nil, fmt.Errorf("%w: unable to generate new and request", err)
-			}
-
-			continue // we will exit on next loop
-		}
-
-		bestAddress, bestBalance, bestCoin, err := c.bestUnlockedSender(ctx, addresses)
-		if err != nil {
-			return "", nil, nil, fmt.Errorf("%w: unable to get best unlocked sender", err)
-		}
-
-		if len(bestAddress) > 0 {
-			return bestAddress, bestBalance, bestCoin, nil
-		}
-
-		broadcasts, err := c.helper.AllBroadcasts(ctx)
-		if err != nil {
-			return "", nil, nil, fmt.Errorf("%w: unable to get broadcasts", err)
-		}
-
-		if len(broadcasts) > 0 {
-			// This condition occurs when we are waiting for some
-			// pending broadcast to complete before creating more
-			// transactions.
-
-			time.Sleep(defaultSleepTime * time.Second)
-			continue
-		}
-
-		if err := c.generateNewAndRequest(ctx); err != nil {
-			return "", nil, nil, fmt.Errorf("%w: generate new address and request", err)
-		}
-	}
-
-	return "", nil, nil, ctx.Err()
-}
-
-// findRecipients returns all possible
-// recipients (address != sender).
-func (c *Constructor) findRecipients(
-	ctx context.Context,
-	sender string,
-) (
-	[]string, // recipients with minimum balance
-	[]string, // recipients without minimum balance
-	error,
-) {
-	minimumRecipients := []string{}
-	belowMinimumRecipients := []string{}
-
-	addresses, err := c.helper.AllAddresses(ctx)
-	if err != nil {
-		return nil, nil, fmt.Errorf("%w: unable to get address", err)
-	}
-	for _, a := range addresses {
-		if a == sender {
-			continue
-		}
-
-		// Sending UTXOs always requires sending to the minimum.
-		if c.accountingModel == configuration.UtxoModel {
-			belowMinimumRecipients = append(belowMinimumRecipients, a)
-
-			continue
-		}
-
-		bal, _, err := c.balance(ctx, a)
-		if err != nil {
-			return nil, nil, fmt.Errorf("%w: unable to retrieve balance for %s", err, a)
-		}
-
-		if new(big.Int).Sub(bal, c.minimumBalance).Sign() >= 0 {
-			minimumRecipients = append(minimumRecipients, a)
-
-			continue
-		}
-
-		belowMinimumRecipients = append(belowMinimumRecipients, a)
-	}
-
-	return minimumRecipients, belowMinimumRecipients, nil
-}
-
-// createScenarioContext creates the context to use
-// for scenario population.
-func (c *Constructor) createScenarioContext(
-	sender string,
-	senderValue *big.Int,
-	recipient string,
-	recipientValue *big.Int,
-	changeAddress string,
-	changeValue *big.Int,
-	coinIdentifier *types.CoinIdentifier,
-) (*scenario.Context, []*types.Operation, error) {
-	// We create a deep copy of the scenaerio (and the change scenario)
-	// to ensure we don't accidentally overwrite the loaded configuration
-	// while hydrating values.
-	scenarioOps := []*types.Operation{}
-	if err := copier.Copy(&scenarioOps, c.scenario); err != nil {
-		return nil, nil, fmt.Errorf("%w: unable to copy scenario", err)
-	}
-
-	if len(changeAddress) > 0 {
-		changeCopy := types.Operation{}
-		if err := copier.Copy(&changeCopy, c.changeScenario); err != nil {
-			return nil, nil, fmt.Errorf("%w: unable to copy change intent", err)
-		}
-
-		scenarioOps = append(scenarioOps, &changeCopy)
-	}
-
-	return &scenario.Context{
-		Sender:         sender,
-		SenderValue:    senderValue,
-		Recipient:      recipient,
-		RecipientValue: recipientValue,
-		Currency:       c.currency,
-		CoinIdentifier: coinIdentifier,
-		ChangeAddress:  changeAddress,
-		ChangeValue:    changeValue,
-	}, scenarioOps, nil
-}
-
-func (c *Constructor) canGetNewAddress(
-	ctx context.Context,
-	recipients []string,
-) (string, bool, error) {
-	addresses, err := c.helper.AllAddresses(ctx)
-	if err != nil {
-		return "", false, fmt.Errorf("%w: unable to get available addresses", err)
-	}
-
-	if len(addresses) >= c.maxAddresses && len(recipients) > 0 {
-		return recipients[0], false, nil
-	}
-
-	randNumber := c.helper.RandomAmount(utils.ZeroInt, utils.OneHundredInt).Int64()
-	convertedAccountProbability := int64(c.newAccountProbability * convertToInt)
-	if randNumber < convertedAccountProbability || len(recipients) == 0 {
-		addr, err := c.newAddress(ctx)
-		if err != nil {
-			return "", false, fmt.Errorf("%w: cannot create new address", err)
-		}
-
-		return addr, true, nil
-	}
-
-	return recipients[0], false, nil
-}
-
-func (c *Constructor) generateAccountScenario(
-	ctx context.Context,
-	sender string,
-	balance *big.Int,
-	minimumRecipients []string,
-	belowMinimumRecipients []string,
-) (
-	*scenario.Context,
-	[]*types.Operation, // scenario operations
-	error, // ErrInsufficientFunds
-) {
-	sendableBalance := new(big.Int).Sub(balance, c.minimumBalance)
-	sendableBalance = new(big.Int).Sub(sendableBalance, c.maximumFee)
-
-	// should send to new account, existing account, or no acccount?
-	if new(big.Int).Sub(balance, c.minimumRequiredBalance(newAccountSend)).Sign() != -1 {
-		recipient, created, err := c.canGetNewAddress(
-			ctx,
-			append(minimumRecipients, belowMinimumRecipients...),
-		)
-		if err != nil {
-			return nil, nil, fmt.Errorf("%w: unable to get recipient", err)
-		}
-
-		if created || utils.ContainsString(belowMinimumRecipients, recipient) {
-			recipientValue := c.helper.RandomAmount(c.minimumBalance, sendableBalance)
-			return c.createScenarioContext(
-				sender,
-				recipientValue,
-				recipient,
-				recipientValue,
-				"",
-				nil,
-				nil,
-			)
-		}
-
-		// We do not need to send the minimum amount here because the recipient
-		// already has a minimum balance.
-		recipientValue := c.helper.RandomAmount(big.NewInt(0), sendableBalance)
-		return c.createScenarioContext(
-			sender,
-			recipientValue,
-			recipient,
-			recipientValue,
-			"",
-			nil,
-			nil,
-		)
-	}
-
-	if new(big.Int).Sub(balance, c.minimumRequiredBalance(existingAccountSend)).Sign() != -1 {
-		if len(minimumRecipients) == 0 {
-			return nil, nil, ErrInsufficientFunds
-		}
-
-		recipientValue := c.helper.RandomAmount(big.NewInt(0), sendableBalance)
-		return c.createScenarioContext(
-			sender,
-			recipientValue,
-			minimumRecipients[0],
-			recipientValue,
-			"",
-			nil,
-			nil,
-		)
-	}
-
-	// Cannot perform any transfer.
-	return nil, nil, ErrInsufficientFunds
-}
-
-func (c *Constructor) generateUtxoScenario(
-	ctx context.Context,
-	sender string,
-	balance *big.Int,
-	recipients []string,
-	coinIdentifier *types.CoinIdentifier,
-) (
-	*scenario.Context,
-	[]*types.Operation, // scenario operations
-	error, // ErrInsufficientFunds
-) {
-	feeLessBalance := new(big.Int).Sub(balance, c.maximumFee)
-	recipient, created, err := c.canGetNewAddress(ctx, recipients)
-	if err != nil {
-		return nil, nil, fmt.Errorf("%w: unable to get recipient", err)
-	}
-
-	// Need to remove from recipients if did not create a recipient address
-	if !created {
-		newRecipients := []string{}
-		for _, r := range recipients {
-			if recipient != r {
-				newRecipients = append(newRecipients, r)
-			}
-		}
-
-		recipients = newRecipients
-	}
-
-	// should send to change, no change, or no send?
-	if new(big.Int).Sub(balance, c.minimumRequiredBalance(changeSend)).Sign() != -1 &&
-		c.changeScenario != nil {
-		changeAddress, _, err := c.canGetNewAddress(ctx, recipients)
-		if err != nil {
-			return nil, nil, fmt.Errorf("%w: unable to get change address", err)
-		}
-
-		doubleMinimumBalance := new(big.Int).Add(c.minimumBalance, c.minimumBalance)
-		changeDifferential := new(big.Int).Sub(feeLessBalance, doubleMinimumBalance)
-
-		recipientShare := c.helper.RandomAmount(big.NewInt(0), changeDifferential)
-		changeShare := new(big.Int).Sub(changeDifferential, recipientShare)
-
-		recipientValue := new(big.Int).Add(c.minimumBalance, recipientShare)
-		changeValue := new(big.Int).Add(c.minimumBalance, changeShare)
-
-		return c.createScenarioContext(
-			sender,
-			balance,
-			recipient,
-			recipientValue,
-			changeAddress,
-			changeValue,
-			coinIdentifier,
-		)
-	}
-
-	if new(big.Int).Sub(balance, c.minimumRequiredBalance(fullSend)).Sign() != -1 {
-		return c.createScenarioContext(
-			sender,
-			balance,
-			recipient,
-			feeLessBalance,
-			"",
-			nil,
-			coinIdentifier,
-		)
-	}
-
-	// Cannot perform any transfer.
-	return nil, nil, ErrInsufficientFunds
-}
-
-// generateScenario determines what should be done in a given
-// transfer based on the sender's balance.
-func (c *Constructor) generateScenario(
-	ctx context.Context,
-	sender string,
-	balance *big.Int,
-	coinIdentifier *types.CoinIdentifier,
-) (
-	*scenario.Context,
-	[]*types.Operation, // scenario operations
-	error, // ErrInsufficientFunds
-) {
-	if balance.Sign() == 0 {
-		// Cannot perform any transfer.
-		return nil, nil, ErrInsufficientFunds
-	}
-
-	minimumRecipients, belowMinimumRecipients, err := c.findRecipients(ctx, sender)
-	if err != nil {
-		return nil, nil, fmt.Errorf("%w: unable to find recipients", err)
-	}
-
-	switch c.accountingModel {
-	case configuration.AccountModel:
-		return c.generateAccountScenario(
-			ctx,
-			sender,
-			balance,
-			minimumRecipients,
-			belowMinimumRecipients,
-		)
-	case configuration.UtxoModel:
-		return c.generateUtxoScenario(ctx, sender, balance, belowMinimumRecipients, coinIdentifier)
-	}
-
-	return nil, nil, ErrInsufficientFunds
-}
-
-func (c *Constructor) generateNewAndRequest(ctx context.Context) error {
-	addr, err := c.newAddress(ctx)
-	if err != nil {
-		return fmt.Errorf("%w: unable to create address", err)
-	}
-
-	_, _, err = c.requestFunds(ctx, addr)
-	if err != nil {
-		return fmt.Errorf("%w: unable to get funds on %s", err, addr)
-	}
-
-	return nil
-}
-
-// CreateTransactions loops on the create transaction loop
-// until the caller cancels the context.
-func (c *Constructor) CreateTransactions(
-	ctx context.Context,
-	clearBroadcasts bool,
-) error {
-	// Before starting loop, delete any pending broadcasts if configuration
-	// indicates to do so.
-	if clearBroadcasts {
-		broadcasts, err := c.helper.ClearBroadcasts(ctx)
-		if err != nil {
-			return fmt.Errorf("%w: unable to clear broadcasts", err)
-		}
-
-		log.Printf(
-			"Cleared pending %d broadcasts: %s\n",
-			len(broadcasts),
-			types.PrettyPrintStruct(broadcasts),
-		)
-	}
-
-	for ctx.Err() == nil {
-		if !c.helper.HeadBlockExists(ctx) {
-			// We will sleep until at least one block has been synced.
-			// Many of the storage-based commands require a synced block
-			// to work correctly (i.e. when fetching a balance, a block
-			// must be returned).
-			time.Sleep(defaultSleepTime * time.Second)
-			continue
-		}
-
-		sender, balance, coinIdentifier, err := c.findSender(ctx)
-		if err != nil {
-			return fmt.Errorf("%w: unable to find sender", err)
-		}
-
-		// Determine Action
-		scenarioCtx, scenarioOps, err := c.generateScenario(
-			ctx,
-			sender,
-			balance,
-			coinIdentifier,
-		)
-		if errors.Is(err, ErrInsufficientFunds) {
-			broadcasts, err := c.helper.AllBroadcasts(ctx)
-			if err != nil {
-				return fmt.Errorf("%w: unable to get broadcasts", err)
-			}
-
-			if len(broadcasts) > 0 {
-				// we will wait for in-flight to process
-				time.Sleep(defaultSleepTime * time.Second)
-				continue
-			}
-
-			if err := c.generateNewAndRequest(ctx); err != nil {
-				return fmt.Errorf("%w: unable to generate new address", err)
-			}
-
-			continue
-		} else if err != nil {
-			return fmt.Errorf("%w: unable to generate intent", err)
-		}
-
-		intent, err := scenario.PopulateScenario(ctx, scenarioCtx, scenarioOps)
-		if err != nil {
-			return fmt.Errorf("%w: unable to populate scenario", err)
-		}
-
-		// Create transaction
-		transactionIdentifier, networkTransaction, err := c.createTransaction(ctx, intent)
-		if err != nil {
-			return fmt.Errorf(
-				"%w: unable to create transaction with operations %s",
-				err,
-				types.PrettyPrintStruct(intent),
-			)
-		}
-
-		logger.LogScenario(scenarioCtx, transactionIdentifier, c.currency)
-
-		// Broadcast Transaction
-		err = c.helper.Broadcast(
-			ctx,
-			sender,
-			intent,
-			transactionIdentifier,
-			networkTransaction,
-		)
-		if err != nil {
-			return fmt.Errorf("%w: unable to enqueue transaction for broadcast", err)
-		}
-
-		if err := c.handler.TransactionCreated(ctx, sender, transactionIdentifier); err != nil {
-			return fmt.Errorf("%w: unable to handle transaction creation", err)
-		}
-	}
-
-	return ctx.Err()
-}
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index c51ebed..053f536 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -22,8 +22,6 @@ import (
 	"os"
 	"path"
 
-	"github.com/coinbase/rosetta-cli/pkg/scenario"
-
 	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/reconciler"
 	"github.com/coinbase/rosetta-sdk-go/statefulsyncer"
@@ -511,30 +509,13 @@ func closeFile(f *os.File) {
 	}
 }
 
-// LogScenario logs what a scenario is perfoming
-// to the console.
-func LogScenario(
-	scenarioCtx *scenario.Context,
+// LogTransactionCreated logs the hash of created
+// transactions.
+func LogTransactionCreated(
 	transactionIdentifier *types.TransactionIdentifier,
-	currency *types.Currency,
 ) {
-	if len(scenarioCtx.ChangeAddress) == 0 {
-		color.Magenta(
-			"Transaction Created: %s\n  %s -- %s --> %s",
-			transactionIdentifier.Hash,
-			scenarioCtx.Sender,
-			utils.PrettyAmount(scenarioCtx.RecipientValue, currency),
-			scenarioCtx.Recipient,
-		)
-	} else {
-		color.Magenta(
-			"Transaction Created: %s\n  %s\n    -- %s --> %s\n    -- %s --> %s",
-			transactionIdentifier.Hash,
-			scenarioCtx.Sender,
-			utils.PrettyAmount(scenarioCtx.RecipientValue, currency),
-			scenarioCtx.Recipient,
-			utils.PrettyAmount(scenarioCtx.ChangeValue, currency),
-			scenarioCtx.ChangeAddress,
-		)
-	}
+	color.Magenta(
+		"Transaction Created: %s\n",
+		transactionIdentifier.Hash,
+	)
 }
diff --git a/pkg/processor/broadcast_storage_handler.go b/pkg/processor/broadcast_storage_handler.go
index 3564ca8..4bfafe7 100644
--- a/pkg/processor/broadcast_storage_handler.go
+++ b/pkg/processor/broadcast_storage_handler.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/coinbase/rosetta-cli/configuration"
 
+	"github.com/coinbase/rosetta-sdk-go/constructor/coordinator"
 	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/storage"
 	"github.com/coinbase/rosetta-sdk-go/types"
@@ -34,6 +35,7 @@ var _ storage.BroadcastStorageHandler = (*BroadcastStorageHandler)(nil)
 type BroadcastStorageHandler struct {
 	config         *configuration.Configuration
 	counterStorage *storage.CounterStorage
+	coordinator    *coordinator.Coordinator
 	parser         *parser.Parser
 }
 
@@ -41,11 +43,13 @@ type BroadcastStorageHandler struct {
 func NewBroadcastStorageHandler(
 	config *configuration.Configuration,
 	counterStorage *storage.CounterStorage,
+	coordinator *coordinator.Coordinator,
 	parser *parser.Parser,
 ) *BroadcastStorageHandler {
 	return &BroadcastStorageHandler{
 		config:         config,
 		counterStorage: counterStorage,
+		coordinator:    coordinator,
 		parser:         parser,
 	}
 }
@@ -54,6 +58,8 @@ func NewBroadcastStorageHandler(
 // last time at a block height < current block height - confirmationDepth.
 func (h *BroadcastStorageHandler) TransactionConfirmed(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
+	identifier string,
 	blockIdentifier *types.BlockIdentifier,
 	transaction *types.Transaction,
 	intent []*types.Operation,
@@ -62,7 +68,21 @@ func (h *BroadcastStorageHandler) TransactionConfirmed(
 		return fmt.Errorf("%w: confirmed transaction did not match intent", err)
 	}
 
-	_, _ = h.counterStorage.Update(ctx, storage.TransactionsConfirmedCounter, big.NewInt(1))
+	_, _ = h.counterStorage.UpdateTransactional(
+		ctx,
+		dbTx,
+		storage.TransactionsConfirmedCounter,
+		big.NewInt(1),
+	)
+
+	if err := h.coordinator.BroadcastComplete(
+		ctx,
+		dbTx,
+		identifier,
+		transaction,
+	); err != nil {
+		return fmt.Errorf("%w: coordinator could not handle transaction", err)
+	}
 
 	return nil
 }
@@ -72,9 +92,16 @@ func (h *BroadcastStorageHandler) TransactionConfirmed(
 // current block height - last broadcast > staleDepth.
 func (h *BroadcastStorageHandler) TransactionStale(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
+	identifier string,
 	transactionIdentifier *types.TransactionIdentifier,
 ) error {
-	_, _ = h.counterStorage.Update(ctx, storage.StaleBroadcastsCounter, big.NewInt(1))
+	_, _ = h.counterStorage.UpdateTransactional(
+		ctx,
+		dbTx,
+		storage.StaleBroadcastsCounter,
+		big.NewInt(1),
+	)
 
 	return nil
 }
@@ -83,10 +110,26 @@ func (h *BroadcastStorageHandler) TransactionStale(
 // put it over the provided broadcast limit.
 func (h *BroadcastStorageHandler) BroadcastFailed(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
+	identifier string,
 	transactionIdentifier *types.TransactionIdentifier,
 	intent []*types.Operation,
 ) error {
-	_, _ = h.counterStorage.Update(ctx, storage.FailedBroadcastsCounter, big.NewInt(1))
+	_, _ = h.counterStorage.UpdateTransactional(
+		ctx,
+		dbTx,
+		storage.FailedBroadcastsCounter,
+		big.NewInt(1),
+	)
+
+	if err := h.coordinator.BroadcastComplete(
+		ctx,
+		dbTx,
+		identifier,
+		nil,
+	); err != nil {
+		return fmt.Errorf("%w: coordinator could not handle transaction", err)
+	}
 
 	if h.config.Construction.IgnoreBroadcastFailures {
 		return nil
diff --git a/pkg/processor/broadcast_storage_helper.go b/pkg/processor/broadcast_storage_helper.go
index 13b4d9d..f8e4f13 100644
--- a/pkg/processor/broadcast_storage_helper.go
+++ b/pkg/processor/broadcast_storage_helper.go
@@ -28,19 +28,16 @@ var _ storage.BroadcastStorageHelper = (*BroadcastStorageHelper)(nil)
 // BroadcastStorageHelper implements the storage.Helper
 // interface.
 type BroadcastStorageHelper struct {
-	network      *types.NetworkIdentifier
 	blockStorage *storage.BlockStorage
 	fetcher      *fetcher.Fetcher
 }
 
 // NewBroadcastStorageHelper returns a new BroadcastStorageHelper.
 func NewBroadcastStorageHelper(
-	network *types.NetworkIdentifier,
 	blockStorage *storage.BlockStorage,
 	fetcher *fetcher.Fetcher,
 ) *BroadcastStorageHelper {
 	return &BroadcastStorageHelper{
-		network:      network,
 		blockStorage: blockStorage,
 		fetcher:      fetcher,
 	}
@@ -92,11 +89,12 @@ func (h *BroadcastStorageHelper) FindTransaction(
 // and returns the *types.TransactionIdentifier returned by the implementation.
 func (h *BroadcastStorageHelper) BroadcastTransaction(
 	ctx context.Context,
+	networkIdentifier *types.NetworkIdentifier,
 	networkTransaction string,
 ) (*types.TransactionIdentifier, error) {
 	transactionIdentifier, _, fetchErr := h.fetcher.ConstructionSubmit(
 		ctx,
-		h.network,
+		networkIdentifier,
 		networkTransaction,
 	)
 	if fetchErr != nil {
diff --git a/pkg/processor/constructor_handler.go b/pkg/processor/coordinator_handler.go
similarity index 57%
rename from pkg/processor/constructor_handler.go
rename to pkg/processor/coordinator_handler.go
index d46613d..2201800 100644
--- a/pkg/processor/constructor_handler.go
+++ b/pkg/processor/coordinator_handler.go
@@ -18,48 +18,34 @@ import (
 	"context"
 	"math/big"
 
-	"github.com/coinbase/rosetta-cli/pkg/constructor"
-
+	"github.com/coinbase/rosetta-sdk-go/constructor/coordinator"
 	"github.com/coinbase/rosetta-sdk-go/storage"
 	"github.com/coinbase/rosetta-sdk-go/types"
 )
 
-var _ constructor.Handler = (*ConstructorHandler)(nil)
+var _ coordinator.Handler = (*CoordinatorHandler)(nil)
 
-// ConstructorHandler is invoked by the Constructor
+// CoordinatorHandler is invoked by the Coordinator
 // when addresses are created or transactions are created.
-type ConstructorHandler struct {
-	balanceStorageHelper *BalanceStorageHelper
-
+type CoordinatorHandler struct {
 	counterStorage *storage.CounterStorage
 }
 
-// NewConstructorHandler returns a new
-// *ConstructorHandler.
-func NewConstructorHandler(
-	balanceStorageHelper *BalanceStorageHelper,
+// NewCoordinatorHandler returns a new
+// *CoordinatorHandler.
+func NewCoordinatorHandler(
 	counterStorage *storage.CounterStorage,
-) *ConstructorHandler {
-	return &ConstructorHandler{
-		balanceStorageHelper: balanceStorageHelper,
-		counterStorage:       counterStorage,
+) *CoordinatorHandler {
+	return &CoordinatorHandler{
+		counterStorage: counterStorage,
 	}
 }
 
-// AddressCreated adds an address to balance tracking.
-func (h *ConstructorHandler) AddressCreated(ctx context.Context, address string) error {
-	h.balanceStorageHelper.AddInterestingAddress(address)
-
-	_, _ = h.counterStorage.Update(ctx, storage.AddressesCreatedCounter, big.NewInt(1))
-
-	return nil
-}
-
 // TransactionCreated increments the TransactionsCreatedCounter in
 // CounterStorage.
-func (h *ConstructorHandler) TransactionCreated(
+func (h *CoordinatorHandler) TransactionCreated(
 	ctx context.Context,
-	sender string,
+	jobIdentifier string,
 	transactionIdentifier *types.TransactionIdentifier,
 ) error {
 	_, _ = h.counterStorage.Update(
diff --git a/pkg/processor/constructor_helper.go b/pkg/processor/coordinator_helper.go
similarity index 62%
rename from pkg/processor/constructor_helper.go
rename to pkg/processor/coordinator_helper.go
index 8675108..8edd7bb 100644
--- a/pkg/processor/constructor_helper.go
+++ b/pkg/processor/coordinator_helper.go
@@ -19,59 +19,77 @@ import (
 	"fmt"
 	"math/big"
 
-	"github.com/coinbase/rosetta-cli/pkg/constructor"
-
+	"github.com/coinbase/rosetta-sdk-go/constructor/coordinator"
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
 	"github.com/coinbase/rosetta-sdk-go/keys"
 	"github.com/coinbase/rosetta-sdk-go/storage"
 	"github.com/coinbase/rosetta-sdk-go/types"
-	"github.com/coinbase/rosetta-sdk-go/utils"
 )
 
-var _ constructor.Helper = (*ConstructorHelper)(nil)
+var _ coordinator.Helper = (*CoordinatorHelper)(nil)
 
-// ConstructorHelper implements the constructor.Helper
+// CoordinatorHelper implements the Coordinator.Helper
 // interface.
-type ConstructorHelper struct {
+type CoordinatorHelper struct {
 	offlineFetcher *fetcher.Fetcher
 	onlineFetcher  *fetcher.Fetcher
 
+	database         storage.Database
 	blockStorage     *storage.BlockStorage
 	keyStorage       *storage.KeyStorage
 	balanceStorage   *storage.BalanceStorage
 	coinStorage      *storage.CoinStorage
 	broadcastStorage *storage.BroadcastStorage
+	counterStorage   *storage.CounterStorage
+
+	balanceStorageHelper *BalanceStorageHelper
 }
 
-// NewConstructorHelper returns a new *ConstructorHelper.
-func NewConstructorHelper(
+// NewCoordinatorHelper returns a new *CoordinatorHelper.
+func NewCoordinatorHelper(
 	offlineFetcher *fetcher.Fetcher,
 	onlineFetcher *fetcher.Fetcher,
+	database storage.Database,
 	blockStorage *storage.BlockStorage,
 	keyStorage *storage.KeyStorage,
 	balanceStorage *storage.BalanceStorage,
 	coinStorage *storage.CoinStorage,
 	broadcastStorage *storage.BroadcastStorage,
-) *ConstructorHelper {
-	return &ConstructorHelper{
-		offlineFetcher:   offlineFetcher,
-		onlineFetcher:    onlineFetcher,
-		blockStorage:     blockStorage,
-		keyStorage:       keyStorage,
-		balanceStorage:   balanceStorage,
-		coinStorage:      coinStorage,
-		broadcastStorage: broadcastStorage,
+	balanceStorageHelper *BalanceStorageHelper,
+	counterStorage *storage.CounterStorage,
+) *CoordinatorHelper {
+	return &CoordinatorHelper{
+		offlineFetcher:       offlineFetcher,
+		onlineFetcher:        onlineFetcher,
+		database:             database,
+		blockStorage:         blockStorage,
+		keyStorage:           keyStorage,
+		balanceStorage:       balanceStorage,
+		coinStorage:          coinStorage,
+		broadcastStorage:     broadcastStorage,
+		counterStorage:       counterStorage,
+		balanceStorageHelper: balanceStorageHelper,
 	}
 }
 
+// DatabaseTransaction returns a new write-ready storage.DatabaseTransaction.
+func (c *CoordinatorHelper) DatabaseTransaction(ctx context.Context) storage.DatabaseTransaction {
+	return c.database.NewDatabaseTransaction(ctx, true)
+}
+
 // Derive returns a new address for a provided publicKey.
-func (c *ConstructorHelper) Derive(
+func (c *CoordinatorHelper) Derive(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	publicKey *types.PublicKey,
 	metadata map[string]interface{},
 ) (string, map[string]interface{}, error) {
-	add, metadata, fetchErr := c.offlineFetcher.ConstructionDerive(ctx, networkIdentifier, publicKey, metadata)
+	add, metadata, fetchErr := c.offlineFetcher.ConstructionDerive(
+		ctx,
+		networkIdentifier,
+		publicKey,
+		metadata,
+	)
 	if fetchErr != nil {
 		return "", nil, fetchErr.Err
 	}
@@ -81,7 +99,7 @@ func (c *ConstructorHelper) Derive(
 
 // Preprocess calls the /construction/preprocess endpoint
 // on an offline node.
-func (c *ConstructorHelper) Preprocess(
+func (c *CoordinatorHelper) Preprocess(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	intent []*types.Operation,
@@ -103,7 +121,7 @@ func (c *ConstructorHelper) Preprocess(
 
 // Metadata calls the /construction/metadata endpoint
 // using the online node.
-func (c *ConstructorHelper) Metadata(
+func (c *CoordinatorHelper) Metadata(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	metadataRequest map[string]interface{},
@@ -123,7 +141,7 @@ func (c *ConstructorHelper) Metadata(
 
 // Payloads calls the /construction/payloads endpoint
 // using the offline node.
-func (c *ConstructorHelper) Payloads(
+func (c *CoordinatorHelper) Payloads(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	intent []*types.Operation,
@@ -145,7 +163,7 @@ func (c *ConstructorHelper) Payloads(
 
 // Parse calls the /construction/parse endpoint
 // using the offline node.
-func (c *ConstructorHelper) Parse(
+func (c *CoordinatorHelper) Parse(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	signed bool,
@@ -167,7 +185,7 @@ func (c *ConstructorHelper) Parse(
 
 // Combine calls the /construction/combine endpoint
 // using the offline node.
-func (c *ConstructorHelper) Combine(
+func (c *CoordinatorHelper) Combine(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	unsignedTransaction string,
@@ -189,7 +207,7 @@ func (c *ConstructorHelper) Combine(
 
 // Hash calls the /construction/hash endpoint
 // using the offline node.
-func (c *ConstructorHelper) Hash(
+func (c *CoordinatorHelper) Hash(
 	ctx context.Context,
 	networkIdentifier *types.NetworkIdentifier,
 	networkTransaction string,
@@ -209,7 +227,7 @@ func (c *ConstructorHelper) Hash(
 
 // Sign invokes the KeyStorage backend
 // to sign some payloads.
-func (c *ConstructorHelper) Sign(
+func (c *CoordinatorHelper) Sign(
 	ctx context.Context,
 	payloads []*types.SigningPayload,
 ) ([]*types.Signature, error) {
@@ -218,103 +236,129 @@ func (c *ConstructorHelper) Sign(
 
 // StoreKey stores a KeyPair and address
 // in KeyStorage.
-func (c *ConstructorHelper) StoreKey(
+func (c *CoordinatorHelper) StoreKey(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
 	address string,
 	keyPair *keys.KeyPair,
 ) error {
-	return c.keyStorage.Store(ctx, address, keyPair)
+	// We optimisically add the interesting address although the dbTx could be reverted.
+	c.balanceStorageHelper.AddInterestingAddress(address)
+
+	_, _ = c.counterStorage.UpdateTransactional(ctx, dbTx, storage.AddressesCreatedCounter, big.NewInt(1))
+	return c.keyStorage.StoreTransactional(ctx, address, keyPair, dbTx)
 }
 
-// AccountBalance returns the balance
+// Balance returns the balance
 // for a provided address using BalanceStorage.
 // If the address balance does not exist,
 // 0 will be returned.
-func (c *ConstructorHelper) AccountBalance(
+func (c *CoordinatorHelper) Balance(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
 	accountIdentifier *types.AccountIdentifier,
 	currency *types.Currency,
-) (*big.Int, error) {
-	amount, _, err := c.balanceStorage.GetBalance(
+) (*types.Amount, error) {
+	amount, _, err := c.balanceStorage.GetBalanceTransactional(
 		ctx,
+		dbTx,
 		accountIdentifier,
 		currency,
 		nil,
 	)
 
-	val, ok := new(big.Int).SetString(amount.Value, 10)
-	if !ok {
-		return nil, fmt.Errorf(
-			"could not parse amount for %s",
-			accountIdentifier.Address,
-		)
-	}
-
-	return val, err
+	return amount, err
 }
 
-// CoinBalance returns the balance of the largest
-// Coin owned by an address.
-func (c *ConstructorHelper) CoinBalance(
+// Coins returns all *types.Coin owned by
+// an account.
+func (c *CoordinatorHelper) Coins(
 	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
 	accountIdentifier *types.AccountIdentifier,
 	currency *types.Currency,
-) (*big.Int, *types.CoinIdentifier, error) {
-	coinValue, coinIdentifier, _, err := c.coinStorage.GetLargestCoin(
+) ([]*types.Coin, error) {
+	coins, _, err := c.coinStorage.GetCoinsTransactional(
 		ctx,
+		dbTx,
 		accountIdentifier,
-		currency,
 	)
+	if err != nil {
+		return nil, fmt.Errorf("%w: unable to get coins", err)
+	}
+
+	coinsToReturn := []*types.Coin{}
+	for _, coin := range coins {
+		if types.Hash(coin.Amount.Currency) != types.Hash(currency) {
+			continue
+		}
 
-	return coinValue, coinIdentifier, err
+		coinsToReturn = append(coinsToReturn, coin)
+	}
+
+	return coinsToReturn, nil
 }
 
 // LockedAddresses returns a slice of all addresses currently sending or receiving
 // funds.
-func (c *ConstructorHelper) LockedAddresses(ctx context.Context) ([]string, error) {
-	return c.broadcastStorage.LockedAddresses(ctx)
+func (c *CoordinatorHelper) LockedAddresses(
+	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
+) ([]string, error) {
+	return c.broadcastStorage.LockedAddresses(ctx, dbTx)
 }
 
 // AllBroadcasts returns a slice of all in-progress broadcasts in BroadcastStorage.
-func (c *ConstructorHelper) AllBroadcasts(ctx context.Context) ([]*storage.Broadcast, error) {
+func (c *CoordinatorHelper) AllBroadcasts(ctx context.Context) ([]*storage.Broadcast, error) {
 	return c.broadcastStorage.GetAllBroadcasts(ctx)
 }
 
 // ClearBroadcasts deletes all pending broadcasts.
-func (c *ConstructorHelper) ClearBroadcasts(ctx context.Context) ([]*storage.Broadcast, error) {
+func (c *CoordinatorHelper) ClearBroadcasts(ctx context.Context) ([]*storage.Broadcast, error) {
 	return c.broadcastStorage.ClearBroadcasts(ctx)
 }
 
 // Broadcast enqueues a particular intent for broadcast.
-func (c *ConstructorHelper) Broadcast(
+func (c *CoordinatorHelper) Broadcast(
 	ctx context.Context,
-	sender string,
+	dbTx storage.DatabaseTransaction,
+	identifier string,
+	network *types.NetworkIdentifier,
 	intent []*types.Operation,
 	transactionIdentifier *types.TransactionIdentifier,
 	payload string,
+	confirmationDepth int64,
 ) error {
 	return c.broadcastStorage.Broadcast(
 		ctx,
-		sender,
+		dbTx,
+		identifier,
+		network,
 		intent,
 		transactionIdentifier,
 		payload,
+		confirmationDepth,
 	)
 }
 
-// AllAddresses returns a slice of all known addresses.
-func (c *ConstructorHelper) AllAddresses(ctx context.Context) ([]string, error) {
-	return c.keyStorage.GetAllAddresses(ctx)
+// BroadcastAll attempts to broadcast all ready transactions.
+func (c *CoordinatorHelper) BroadcastAll(
+	ctx context.Context,
+) error {
+	return c.broadcastStorage.BroadcastAll(ctx, true)
 }
 
-// RandomAmount returns some integer between min and max.
-func (c *ConstructorHelper) RandomAmount(min *big.Int, max *big.Int) *big.Int {
-	return utils.RandomNumber(min, max)
+// AllAddresses returns a slice of all known addresses.
+func (c *CoordinatorHelper) AllAddresses(
+	ctx context.Context,
+	dbTx storage.DatabaseTransaction,
+) ([]string, error) {
+	return c.keyStorage.GetAllAddressesTransactional(ctx, dbTx)
 }
 
 // HeadBlockExists returns a boolean indicating if a block has been
 // synced by BlockStorage.
-func (c *ConstructorHelper) HeadBlockExists(ctx context.Context) bool {
+func (c *CoordinatorHelper) HeadBlockExists(ctx context.Context) bool {
 	headBlock, _ := c.blockStorage.GetHeadBlockIdentifier(ctx)
 
 	return headBlock != nil
diff --git a/pkg/processor/reconciler_helper.go b/pkg/processor/reconciler_helper.go
index 96efb8a..a23e563 100644
--- a/pkg/processor/reconciler_helper.go
+++ b/pkg/processor/reconciler_helper.go
@@ -97,7 +97,14 @@ func (h *ReconcilerHelper) LiveBalance(
 	currency *types.Currency,
 	headBlock *types.BlockIdentifier,
 ) (*types.Amount, *types.BlockIdentifier, error) {
-	amt, block, _, err := utils.CurrencyBalance(ctx, h.network, h.fetcher, account, currency, headBlock)
+	amt, block, _, err := utils.CurrencyBalance(
+		ctx,
+		h.network,
+		h.fetcher,
+		account,
+		currency,
+		headBlock,
+	)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/scenario/scenario.go b/pkg/scenario/scenario.go
deleted file mode 100644
index 5dd2e95..0000000
--- a/pkg/scenario/scenario.go
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// 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 scenario
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"math/big"
-	"strings"
-
-	"github.com/coinbase/rosetta-sdk-go/types"
-)
-
-const (
-	// Scenarios can contain one of many of the following reserved
-	// keywords that are automatically populated.
-
-	// Sender is the sender and signer of a transaction.
-	Sender = "{{ SENDER }}"
-
-	// SenderValue is the amount the sender is paying.
-	SenderValue = "{{ SENDER_VALUE }}"
-
-	// Recipient is the recipient of the transaction.
-	Recipient = "{{ RECIPIENT }}"
-
-	// RecipientValue is the amount the recipient is
-	// receiving from the sender. Note, this is distinct
-	// from the SenderValue so that UTXO transfers
-	// can be supported.
-	RecipientValue = "{{ RECIPIENT_VALUE }}"
-
-	// CoinIdentifier is the globally unique identifier
-	// of a UTXO. This should be in the Operation.metadata
-	// of any UTXO-based blockchain ("utxo_created" when
-	// a new UTXO is created and "utxo_spent" when a
-	// UTXO is spent).
-	CoinIdentifier = "{{ COIN_IDENTIFIER }}"
-
-	// ChangeAddress is the recipient of change in a
-	// UTXO transaction (surplus above recipient value).
-	// TODO: generalize this to allow for generic
-	// one-to-many sends.
-	ChangeAddress = "{{ CHANGE_ADDRESS }}"
-
-	// ChangeValue is the amount to send to the change
-	// address.
-	ChangeValue = "{{ CHANGE_VALUE }}"
-)
-
-// Context is all information passed to PopulateScenario.
-// As more exotic scenario testing is supported, this will
-// likely be expanded.
-type Context struct {
-	Sender         string
-	SenderValue    *big.Int
-	Recipient      string
-	RecipientValue *big.Int
-	CoinIdentifier *types.CoinIdentifier
-	Currency       *types.Currency
-	ChangeAddress  string
-	ChangeValue    *big.Int
-}
-
-// PopulateScenario populates a provided scenario (slice of
-// []*types.Operation) with the information in Context.
-func PopulateScenario(
-	ctx context.Context,
-	scenarioContext *Context,
-	scenario []*types.Operation,
-) ([]*types.Operation, error) {
-	// Convert operations to a string
-	bytes, err := json.Marshal(scenario)
-	if err != nil {
-		return nil, fmt.Errorf("%w: unable to marshal scenario", err)
-	}
-
-	// Replace all keywords with information in Context
-	stringBytes := string(bytes)
-	stringBytes = strings.ReplaceAll(stringBytes, Sender, scenarioContext.Sender)
-	stringBytes = strings.ReplaceAll(
-		stringBytes,
-		SenderValue,
-		new(big.Int).Neg(scenarioContext.SenderValue).String(),
-	)
-	stringBytes = strings.ReplaceAll(stringBytes, Recipient, scenarioContext.Recipient)
-	stringBytes = strings.ReplaceAll(
-		stringBytes,
-		RecipientValue,
-		new(big.Int).Abs(scenarioContext.RecipientValue).String(),
-	)
-
-	if scenarioContext.CoinIdentifier != nil {
-		stringBytes = strings.ReplaceAll(
-			stringBytes,
-			CoinIdentifier,
-			scenarioContext.CoinIdentifier.Identifier,
-		)
-	}
-
-	if len(scenarioContext.ChangeAddress) > 0 {
-		stringBytes = strings.ReplaceAll(
-			stringBytes,
-			ChangeAddress,
-			scenarioContext.ChangeAddress,
-		)
-
-		stringBytes = strings.ReplaceAll(
-			stringBytes,
-			ChangeValue,
-			new(big.Int).Abs(scenarioContext.ChangeValue).String(),
-		)
-	}
-
-	// Convert back to ops
-	var ops []*types.Operation
-	if err := json.Unmarshal([]byte(stringBytes), &ops); err != nil {
-		return nil, fmt.Errorf("%w: unable to unmarshal ops", err)
-	}
-
-	// Post-process operations
-	for _, op := range ops {
-		if op.Amount != nil {
-			op.Amount.Currency = scenarioContext.Currency
-		}
-	}
-
-	return ops, nil
-}


Test patch:
diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go
index cef51e4..5ec55bf 100644
--- a/configuration/configuration_test.go
+++ b/configuration/configuration_test.go
@@ -19,6 +19,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/coinbase/rosetta-sdk-go/constructor/job"
 	"github.com/coinbase/rosetta-sdk-go/storage"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"github.com/coinbase/rosetta-sdk-go/utils"
@@ -31,7 +32,17 @@ var (
 	goodCoverage  = float64(0.33)
 	badCoverage   = float64(-2)
 	endTip        = false
-	whackyConfig  = &Configuration{
+	fakeWorkflows = []*job.Workflow{
+		{
+			Name:        string(job.CreateAccount),
+			Concurrency: job.ReservedWorkflowConcurrency,
+		},
+		{
+			Name:        string(job.RequestFunds),
+			Concurrency: job.ReservedWorkflowConcurrency,
+		},
+	}
+	whackyConfig = &Configuration{
 		Network: &types.NetworkIdentifier{
 			Blockchain: "sweet",
 			Network:    "sweeter",
@@ -43,23 +54,17 @@ var (
 		TransactionConcurrency: 2,
 		TipDelay:               1231,
 		Construction: &ConstructionConfiguration{
-			OfflineURL: "https://ashdjaksdkjshdk",
-			Currency: &types.Currency{
-				Symbol:   "FIRE",
-				Decimals: 100,
-			},
-			MinimumBalance:        "1002",
-			MaximumFee:            "1",
-			CurveType:             types.Edwards25519,
-			AccountingModel:       UtxoModel,
-			Scenario:              EthereumTransfer,
-			ChangeScenario:        EthereumTransfer[0],
-			ConfirmationDepth:     100,
-			StaleDepth:            12,
-			BroadcastLimit:        200,
-			BlockBroadcastLimit:   992,
-			NewAccountProbability: 0.1,
-			MaxAddresses:          12,
+			OfflineURL:          "https://ashdjaksdkjshdk",
+			StaleDepth:          12,
+			BroadcastLimit:      200,
+			BlockBroadcastLimit: 992,
+			Workflows: append(
+				fakeWorkflows,
+				&job.Workflow{
+					Name:        "transfer",
+					Concurrency: 100,
+				},
+			),
 		},
 		Data: &DataConfiguration{
 			ActiveReconciliationConcurrency:   100,
@@ -78,34 +83,6 @@ var (
 			Blockchain: "?",
 		},
 	}
-	invalidCurrency = &Configuration{
-		Construction: &ConstructionConfiguration{
-			Currency: &types.Currency{
-				Decimals: 12,
-			},
-		},
-	}
-	invalidCurve = &Configuration{
-		Construction: &ConstructionConfiguration{
-			CurveType: "hello",
-		},
-	}
-	invalidAccountingModel = &Configuration{
-		Construction: &ConstructionConfiguration{
-			AccountingModel: "hello",
-		},
-	}
-	invalidMinimumBalance = &Configuration{
-		Construction: &ConstructionConfiguration{
-			MinimumBalance: "-1000",
-		},
-	}
-	invalidMaximumFee = &Configuration{
-		Construction: &ConstructionConfiguration{
-			MaximumFee: "hello",
-		},
-	}
-
 	invalidPrefundedAccounts = &Configuration{
 		Construction: &ConstructionConfiguration{
 			PrefundedAccounts: []*storage.PrefundedAccount{
@@ -161,35 +138,28 @@ func TestLoadConfiguration(t *testing.T) {
 		},
 		"overwrite missing": {
 			provided: &Configuration{
-				Construction: &ConstructionConfiguration{},
-				Data:         &DataConfiguration{},
+				Construction: &ConstructionConfiguration{
+					Workflows: fakeWorkflows,
+				},
+				Data: &DataConfiguration{},
 			},
-			expected: DefaultConfiguration(),
+			expected: func() *Configuration {
+				cfg := DefaultConfiguration()
+				cfg.Construction = &ConstructionConfiguration{
+					OfflineURL:          DefaultURL,
+					StaleDepth:          DefaultStaleDepth,
+					BroadcastLimit:      DefaultBroadcastLimit,
+					BlockBroadcastLimit: DefaultBlockBroadcastLimit,
+					Workflows:           fakeWorkflows,
+				}
+
+				return cfg
+			}(),
 		},
 		"invalid network": {
 			provided: invalidNetwork,
 			err:      true,
 		},
-		"invalid currency": {
-			provided: invalidCurrency,
-			err:      true,
-		},
-		"invalid curve type": {
-			provided: invalidCurve,
-			err:      true,
-		},
-		"invalid accounting model": {
-			provided: invalidAccountingModel,
-			err:      true,
-		},
-		"invalid minimum balance": {
-			provided: invalidMinimumBalance,
-			err:      true,
-		},
-		"invalid maximum fee": {
-			provided: invalidMaximumFee,
-			err:      true,
-		},
 		"invalid prefunded accounts": {
 			provided: invalidPrefundedAccounts,
 			err:      true,
@@ -239,6 +209,14 @@ func TestLoadConfiguration(t *testing.T) {
 			},
 			err: true,
 		},
+		"missing reserved workflows": {
+			provided: &Configuration{
+				Construction: &ConstructionConfiguration{
+					Workflows: []*job.Workflow{},
+				},
+			},
+			err: true,
+		},
 		"multiple end conditions": {
 			provided: multipleEndConditions,
 			expected: func() *Configuration {
diff --git a/pkg/constructor/constructor_test.go b/pkg/constructor/constructor_test.go
deleted file mode 100644
index 0dab0aa..0000000
--- a/pkg/constructor/constructor_test.go
+++ /dev/null
@@ -1,1475 +0,0 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// 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 constructor
-
-import (
-	"context"
-	"fmt"
-	"math/big"
-	"testing"
-	"time"
-
-	"github.com/coinbase/rosetta-cli/configuration"
-	mocks "github.com/coinbase/rosetta-cli/mocks/constructor"
-	"github.com/coinbase/rosetta-cli/pkg/scenario"
-
-	"github.com/coinbase/rosetta-sdk-go/asserter"
-	"github.com/coinbase/rosetta-sdk-go/parser"
-	"github.com/coinbase/rosetta-sdk-go/storage"
-	"github.com/coinbase/rosetta-sdk-go/types"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-)
-
-func simpleAsserterConfiguration() (*asserter.Asserter, error) {
-	return asserter.NewClientWithOptions(
-		&types.NetworkIdentifier{
-			Blockchain: "bitcoin",
-			Network:    "mainnet",
-		},
-		&types.BlockIdentifier{
-			Hash:  "block 0",
-			Index: 0,
-		},
-		[]string{"Transfer"},
-		[]*types.OperationStatus{
-			{
-				Status:     "success",
-				Successful: true,
-			},
-			{
-				Status:     "failure",
-				Successful: false,
-			},
-		},
-		[]*types.Error{},
-	)
-}
-
-func defaultParser(t *testing.T) *parser.Parser {
-	asserter, err := simpleAsserterConfiguration()
-	assert.NoError(t, err)
-
-	return parser.New(asserter, nil)
-}
-
-func defaultAccountConstructor(t *testing.T) (*Constructor, *mocks.Helper, *mocks.Handler) {
-	helper := new(mocks.Helper)
-	handler := new(mocks.Handler)
-	return &Constructor{
-		network: &types.NetworkIdentifier{
-			Blockchain: "Ethereum",
-			Network:    "Mainnet",
-		},
-		accountingModel: configuration.AccountModel,
-		currency: &types.Currency{
-			Symbol:   "ETH",
-			Decimals: 18,
-		},
-		minimumBalance:        big.NewInt(2),
-		maximumFee:            big.NewInt(100),
-		curveType:             types.Secp256k1,
-		newAccountProbability: 0.5,
-		maxAddresses:          100,
-		scenario:              configuration.EthereumTransfer,
-		parser:                defaultParser(t),
-		helper:                helper,
-		handler:               handler,
-	}, helper, handler
-}
-
-func bitcoinScenarios() ([]*types.Operation, *types.Operation) {
-	changelessTransfer := []*types.Operation{
-		{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 0,
-			},
-			Type: "Vin",
-			Account: &types.AccountIdentifier{
-				Address: "{{ SENDER }}",
-			},
-			Amount: &types.Amount{
-				Value: "{{ SENDER_VALUE }}",
-			},
-		},
-		{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 1,
-			},
-			Type: "Vout",
-			Account: &types.AccountIdentifier{
-				Address: "{{ RECIPIENT }}",
-			},
-			Amount: &types.Amount{
-				Value: "{{ RECIPIENT_VALUE }}",
-			},
-		},
-	}
-
-	change := &types.Operation{
-		OperationIdentifier: &types.OperationIdentifier{
-			Index: 2,
-		},
-		Type: "Vout",
-		Account: &types.AccountIdentifier{
-			Address: "{{ CHANGE_ADDRESS }}",
-		},
-		Amount: &types.Amount{
-			Value: "{{ CHANGE_VALUE }}",
-		},
-	}
-
-	return changelessTransfer, change
-}
-
-func defaultUtxoConstructor(t *testing.T) (*Constructor, *mocks.Helper) {
-	helper := new(mocks.Helper)
-	changelessTransfer, change := bitcoinScenarios()
-	return &Constructor{
-		network: &types.NetworkIdentifier{
-			Blockchain: "Bitcoin",
-			Network:    "Mainnet",
-		},
-		accountingModel: configuration.UtxoModel,
-		currency: &types.Currency{
-			Symbol:   "BTC",
-			Decimals: 8,
-		},
-		minimumBalance:        big.NewInt(600),
-		maximumFee:            big.NewInt(500),
-		curveType:             types.Secp256k1,
-		newAccountProbability: 0.5,
-		maxAddresses:          100,
-		scenario:              changelessTransfer,
-		changeScenario:        change,
-		parser:                defaultParser(t),
-		helper:                helper,
-	}, helper
-}
-
-var (
-	sender    = "sender"
-	recipient = "recipient"
-)
-
-func TestNewAddress(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, mockHandler := defaultAccountConstructor(t)
-
-	mockHelper.On(
-		"Derive",
-		ctx,
-		constructor.network,
-		mock.Anything,
-		mock.Anything,
-	).Return(
-		"addr 1",
-		nil,
-		nil,
-	)
-	mockHelper.On("StoreKey", ctx, "addr 1", mock.Anything).Return(nil)
-	mockHandler.On("AddressCreated", ctx, "addr 1").Return(nil)
-
-	addr, err := constructor.newAddress(ctx)
-	assert.Equal(t, "addr 1", addr)
-	assert.NoError(t, err)
-}
-
-func TestCreateTransaction(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, _ := defaultAccountConstructor(t)
-
-	senderValue := big.NewInt(100)
-	recipientValue := big.NewInt(90)
-
-	scenarioContext, scenarioOps, err := constructor.createScenarioContext(
-		sender,
-		senderValue,
-		recipient,
-		recipientValue,
-		"",
-		nil,
-		nil,
-	)
-	assert.NotNil(t, scenarioContext)
-	assert.NotNil(t, scenarioOps)
-	assert.NoError(t, err)
-
-	intent, err := scenario.PopulateScenario(ctx, scenarioContext, scenarioOps)
-	assert.NoError(t, err)
-	assert.NotNil(t, intent)
-
-	metadataRequest := map[string]interface{}{
-		"meta": "data",
-	}
-
-	metadataResponse := map[string]interface{}{
-		"interesting": "stuff",
-	}
-
-	unsignedTransaction := "unsigned transaction"
-	payloads := []*types.SigningPayload{
-		{
-			Address:       sender,
-			Bytes:         []byte("signing payload"),
-			SignatureType: types.Ecdsa,
-		},
-	}
-
-	generatedOps := append(intent, &types.Operation{
-		OperationIdentifier: &types.OperationIdentifier{
-			Index: 2,
-		},
-		Type: "fee",
-		Account: &types.AccountIdentifier{
-			Address: sender,
-		},
-		Amount: &types.Amount{
-			Value:    constructor.maximumFee.String(),
-			Currency: constructor.currency,
-		},
-	})
-
-	signatures := []*types.Signature{
-		{
-			SigningPayload: payloads[0],
-			PublicKey:      &types.PublicKey{},
-			SignatureType:  types.Ecdsa,
-			Bytes:          []byte("signature"),
-		},
-	}
-
-	signedTransaction := "signed transaction"
-
-	transactionIdentifier := &types.TransactionIdentifier{
-		Hash: "transaction hash",
-	}
-
-	mockHelper.On(
-		"Preprocess",
-		ctx,
-		constructor.network,
-		intent,
-		mock.Anything,
-	).Return(
-		metadataRequest,
-		nil,
-	)
-	mockHelper.On(
-		"Metadata",
-		ctx,
-		constructor.network,
-		metadataRequest,
-	).Return(
-		metadataResponse,
-		nil,
-	)
-	mockHelper.On(
-		"Payloads",
-		ctx,
-		constructor.network,
-		intent,
-		metadataResponse,
-	).Return(
-		unsignedTransaction,
-		payloads,
-		nil,
-	)
-	mockHelper.On(
-		"Parse",
-		ctx,
-		constructor.network,
-		false,
-		unsignedTransaction,
-	).Return(
-		generatedOps,
-		[]string{},
-		nil,
-		nil,
-	)
-	mockHelper.On("Sign", ctx, payloads).Return(signatures, nil)
-	mockHelper.On(
-		"Combine",
-		ctx,
-		constructor.network,
-		unsignedTransaction,
-		signatures,
-	).Return(
-		signedTransaction,
-		nil,
-	)
-	mockHelper.On(
-		"Parse",
-		ctx,
-		constructor.network,
-		true,
-		signedTransaction,
-	).Return(
-		generatedOps,
-		[]string{sender},
-		nil,
-		nil,
-	)
-	mockHelper.On(
-		"Hash",
-		ctx,
-		constructor.network,
-		signedTransaction,
-	).Return(
-		transactionIdentifier,
-		nil,
-	)
-
-	resultIdentifier, networkTransaction, err := constructor.createTransaction(ctx, intent)
-	assert.NoError(t, err)
-	assert.Equal(t, transactionIdentifier, resultIdentifier)
-	assert.Equal(t, signedTransaction, networkTransaction)
-}
-
-func TestMinimumRequiredBalance_Account(t *testing.T) {
-	constructor, _, _ := defaultAccountConstructor(t)
-
-	// 2 * minimum_balance + maximum_fee
-	assert.Equal(t, big.NewInt(104), constructor.minimumRequiredBalance(newAccountSend))
-
-	// minimum_balance + maximum_fee
-	assert.Equal(t, big.NewInt(102), constructor.minimumRequiredBalance(existingAccountSend))
-}
-
-func TestMinimumRequiredBalance_Utxo(t *testing.T) {
-	constructor, _ := defaultUtxoConstructor(t)
-
-	// 2 * minimum_balance + maximum_fee
-	assert.Equal(t, big.NewInt(1700), constructor.minimumRequiredBalance(changeSend))
-
-	// minimum_balance + maximum_fee
-	assert.Equal(t, big.NewInt(1100), constructor.minimumRequiredBalance(fullSend))
-}
-
-func TestBestUnlockedSender_Account(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, _ := defaultAccountConstructor(t)
-
-	lockedAddresses := []string{"addr 2", "addr 4"}
-	mockHelper.On("LockedAddresses", ctx).Return(lockedAddresses, nil)
-
-	balances := map[string]*big.Int{
-		"addr 1": big.NewInt(10),
-		"addr 2": big.NewInt(30),
-		"addr 3": big.NewInt(15),
-		"addr 4": big.NewInt(1000),
-		"addr 5": big.NewInt(2),
-	}
-	addresses := []string{}
-	for k := range balances {
-		mockHelper.On(
-			"AccountBalance",
-			ctx,
-			&types.AccountIdentifier{Address: k},
-			constructor.currency,
-		).Return(
-			balances[k],
-			nil,
-		)
-		addresses = append(addresses, k)
-	}
-
-	bestAddress, bestBalance, bestCoin, err := constructor.bestUnlockedSender(ctx, addresses)
-	assert.NoError(t, err)
-	assert.Equal(t, "addr 3", bestAddress)
-	assert.Equal(t, big.NewInt(15), bestBalance)
-	assert.Nil(t, bestCoin)
-}
-
-func TestBestUnlockedSender_Utxo(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper := defaultUtxoConstructor(t)
-
-	lockedAddresses := []string{"addr 2", "addr 4"}
-	mockHelper.On("LockedAddresses", ctx).Return(lockedAddresses, nil)
-
-	type intAndCoin struct {
-		amount *big.Int
-		coin   *types.CoinIdentifier
-	}
-
-	balances := map[string]*intAndCoin{
-		"addr 1": {
-			amount: big.NewInt(10),
-			coin:   &types.CoinIdentifier{Identifier: "coin 1"},
-		},
-		"addr 2": {
-			amount: big.NewInt(30),
-			coin:   &types.CoinIdentifier{Identifier: "coin 2"},
-		},
-		"addr 3": {
-			amount: big.NewInt(15),
-			coin:   &types.CoinIdentifier{Identifier: "coin 3"},
-		},
-		"addr 4": {
-			amount: big.NewInt(1000),
-			coin:   &types.CoinIdentifier{Identifier: "coin 4"},
-		},
-		"addr 5": {
-			amount: big.NewInt(2),
-			coin:   &types.CoinIdentifier{Identifier: "coin 5"},
-		},
-	}
-	addresses := []string{}
-	for k := range balances {
-		mockHelper.On(
-			"CoinBalance",
-			ctx,
-			&types.AccountIdentifier{Address: k},
-			constructor.currency,
-		).Return(
-			balances[k].amount,
-			balances[k].coin,
-			nil,
-		)
-		addresses = append(addresses, k)
-	}
-
-	bestAddress, bestBalance, bestCoin, err := constructor.bestUnlockedSender(ctx, addresses)
-	assert.NoError(t, err)
-	assert.Equal(t, "addr 3", bestAddress)
-	assert.Equal(t, big.NewInt(15), bestBalance)
-	assert.Equal(t, "coin 3", bestCoin.Identifier)
-}
-
-func TestFindSender_Load(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, mockHandler := defaultAccountConstructor(t)
-
-	lockedAddresses := []string{}
-	mockHelper.On("LockedAddresses", ctx).Return(lockedAddresses, nil)
-
-	mockHelper.On("AllAddresses", ctx).Return([]string{}, nil).Once()
-
-	mockHelper.On(
-		"Derive",
-		ctx,
-		constructor.network,
-		mock.Anything,
-		mock.Anything,
-	).Return(
-		"addr 1",
-		nil,
-		nil,
-	)
-	mockHelper.On("StoreKey", ctx, "addr 1", mock.Anything).Return(nil)
-	mockHandler.On("AddressCreated", ctx, "addr 1").Return(nil)
-
-	// Deposit insufficient funds
-	mockHelper.On(
-		"AccountBalance",
-		ctx,
-		&types.AccountIdentifier{Address: "addr 1"},
-		constructor.currency,
-	).Return(
-		big.NewInt(100),
-		nil,
-	).After(5 * time.Second).Once()
-
-	// Deposit sufficient funds
-	mockHelper.On(
-		"AccountBalance",
-		ctx,
-		&types.AccountIdentifier{Address: "addr 1"},
-		constructor.currency,
-	).Return(
-		big.NewInt(10000),
-		nil,
-	).After(15 * time.Second).Twice()
-	mockHelper.On("AllAddresses", ctx).Return([]string{"addr 1"}, nil)
-
-	sender, balance, coin, err := constructor.findSender(ctx)
-	mockHelper.AssertExpectations(t)
-	assert.NoError(t, err)
-	assert.Equal(t, "addr 1", sender)
-	assert.Equal(t, big.NewInt(10000), balance)
-	assert.Nil(t, coin)
-}
-
-func TestFindSender_Available(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, _ := defaultAccountConstructor(t)
-
-	lockedAddresses := []string{"addr 2", "addr 4"}
-	mockHelper.On("LockedAddresses", ctx).Return(lockedAddresses, nil)
-
-	balances := map[string]*big.Int{
-		"addr 1": big.NewInt(10),
-		"addr 2": big.NewInt(30),
-		"addr 3": big.NewInt(15),
-		"addr 4": big.NewInt(1000),
-		"addr 5": big.NewInt(2),
-	}
-	addresses := []string{}
-	for k := range balances {
-		mockHelper.On(
-			"AccountBalance",
-			ctx,
-			&types.AccountIdentifier{Address: k},
-			constructor.currency,
-		).Return(
-			balances[k],
-			nil,
-		)
-		addresses = append(addresses, k)
-	}
-
-	mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-
-	sender, balance, coin, err := constructor.findSender(ctx)
-	assert.NoError(t, err)
-	assert.Equal(t, "addr 3", sender)
-	assert.Equal(t, big.NewInt(15), balance)
-	assert.Nil(t, coin)
-}
-
-func TestFindSender_Broadcasting(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, _ := defaultAccountConstructor(t)
-
-	// This will trigger a loop while waiting for broadcasts
-	mockHelper.On(
-		"LockedAddresses",
-		ctx,
-	).Return(
-		[]string{"addr 1", "addr 2", "addr 3", "addr 4", "addr 5"},
-		nil,
-	).Once()
-
-	balances := map[string]*big.Int{
-		"addr 1": big.NewInt(10),
-		"addr 2": big.NewInt(30),
-		"addr 3": big.NewInt(15),
-		"addr 4": big.NewInt(1000),
-		"addr 5": big.NewInt(2),
-	}
-	addresses := []string{}
-	for k := range balances {
-		addresses = append(addresses, k)
-	}
-
-	mockHelper.On(
-		"AccountBalance",
-		ctx,
-		&types.AccountIdentifier{Address: "addr 3"},
-		constructor.currency,
-	).Return(
-		balances["addr 3"],
-		nil,
-	)
-
-	mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-	mockHelper.On("AllBroadcasts", ctx).Return([]*storage.Broadcast{{}}, nil)
-
-	// After first fail, addr 3 will be available
-	mockHelper.On(
-		"LockedAddresses",
-		ctx,
-	).Return(
-		[]string{"addr 1", "addr 2", "addr 4", "addr 5"},
-		nil,
-	).After(
-		8 * time.Second,
-	)
-
-	sender, balance, coin, err := constructor.findSender(ctx)
-	mockHelper.AssertExpectations(t)
-	assert.NoError(t, err)
-	assert.Equal(t, "addr 3", sender)
-	assert.Equal(t, big.NewInt(15), balance)
-	assert.Nil(t, coin)
-}
-
-func TestFindRecipients_Account(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper, _ := defaultAccountConstructor(t)
-	constructor.minimumBalance = big.NewInt(15)
-
-	balances := map[string]*big.Int{
-		"addr 1": big.NewInt(10),
-		"addr 2": big.NewInt(30),
-		"addr 3": big.NewInt(15),
-		"addr 4": big.NewInt(1000),
-		"addr 5": big.NewInt(2),
-	}
-	addresses := []string{}
-	for k := range balances {
-		mockHelper.On(
-			"AccountBalance",
-			ctx,
-			&types.AccountIdentifier{Address: k},
-			constructor.currency,
-		).Return(
-			balances[k],
-			nil,
-		)
-		addresses = append(addresses, k)
-	}
-
-	mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-
-	minimumRecipients, belowMinimumRecipients, err := constructor.findRecipients(ctx, "addr 4")
-	assert.NoError(t, err)
-	assert.ElementsMatch(t, []string{"addr 2", "addr 3"}, minimumRecipients)
-	assert.ElementsMatch(t, []string{"addr 1", "addr 5"}, belowMinimumRecipients)
-}
-
-type intAndCoin struct {
-	amount *big.Int
-	coin   *types.CoinIdentifier
-}
-
-func TestFindRecipients_Utxo(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, mockHelper := defaultUtxoConstructor(t)
-
-	balances := map[string]*intAndCoin{
-		"addr 1": {
-			amount: big.NewInt(10),
-			coin:   &types.CoinIdentifier{Identifier: "coin 1"},
-		},
-		"addr 2": {
-			amount: big.NewInt(30),
-			coin:   &types.CoinIdentifier{Identifier: "coin 2"},
-		},
-		"addr 3": {
-			amount: big.NewInt(15),
-			coin:   &types.CoinIdentifier{Identifier: "coin 3"},
-		},
-		"addr 4": {
-			amount: big.NewInt(1000),
-			coin:   &types.CoinIdentifier{Identifier: "coin 4"},
-		},
-		"addr 5": {
-			amount: big.NewInt(2),
-			coin:   &types.CoinIdentifier{Identifier: "coin 5"},
-		},
-	}
-	addresses := []string{}
-	for k := range balances {
-		mockHelper.On(
-			"CoinBalance",
-			ctx,
-			&types.AccountIdentifier{Address: k},
-			constructor.currency,
-		).Return(
-			balances[k].amount,
-			balances[k].coin,
-			nil,
-		)
-		addresses = append(addresses, k)
-	}
-
-	mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-
-	minimumRecipients, belowMinimumRecipients, err := constructor.findRecipients(ctx, "addr 4")
-	assert.NoError(t, err)
-	assert.ElementsMatch(t, []string{}, minimumRecipients)
-	assert.ElementsMatch(
-		t,
-		[]string{"addr 1", "addr 2", "addr 3", "addr 5"},
-		belowMinimumRecipients,
-	)
-}
-
-func TestCreateScenarioContext_Account(t *testing.T) {
-	constructor, _, _ := defaultAccountConstructor(t)
-
-	senderValue := big.NewInt(5000)
-	recipientValue := big.NewInt(2000)
-
-	scenarioContext, scenarioOps, err := constructor.createScenarioContext(
-		sender,
-		senderValue,
-		recipient,
-		recipientValue,
-		"",
-		nil,
-		nil,
-	)
-	assert.NoError(t, err)
-	assert.Equal(t, &scenario.Context{
-		Sender:         sender,
-		SenderValue:    senderValue,
-		Recipient:      recipient,
-		RecipientValue: recipientValue,
-		Currency:       constructor.currency,
-	}, scenarioContext)
-	assert.Equal(t, constructor.scenario, scenarioOps)
-
-	// ensure copied correctly
-	constructor.scenario[0].Type = "blah"
-	assert.NotEqual(t, constructor.scenario[0], scenarioOps[0])
-}
-
-func TestCreateScenarioContext_Utxo(t *testing.T) {
-	constructor, _ := defaultUtxoConstructor(t)
-
-	senderValue := big.NewInt(5000)
-	recipientValue := big.NewInt(2000)
-	changeAddress := "change"
-	changeValue := big.NewInt(1900)
-	coinIdentifier := &types.CoinIdentifier{Identifier: "coin 2"}
-
-	scenarioContext, scenarioOps, err := constructor.createScenarioContext(
-		sender,
-		senderValue,
-		recipient,
-		recipientValue,
-		changeAddress,
-		changeValue,
-		coinIdentifier,
-	)
-	assert.NoError(t, err)
-	assert.Equal(t, &scenario.Context{
-		Sender:         sender,
-		SenderValue:    senderValue,
-		Recipient:      recipient,
-		RecipientValue: recipientValue,
-		ChangeAddress:  changeAddress,
-		ChangeValue:    changeValue,
-		CoinIdentifier: coinIdentifier,
-		Currency:       constructor.currency,
-	}, scenarioContext)
-	assert.Equal(t, append(constructor.scenario, constructor.changeScenario), scenarioOps)
-
-	// ensure copied correctly
-	constructor.scenario[0].Type = "blah"
-	assert.NotEqual(t, constructor.scenario[0], scenarioOps[0])
-
-	constructor.changeScenario.Type = "blah 2"
-	assert.NotEqual(t, constructor.changeScenario, scenarioOps[2])
-}
-
-func TestCanGetNewAddress(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, _, mockHandler := defaultAccountConstructor(t)
-
-	var tests = map[string]struct {
-		addresses             int
-		randomAmount          *big.Int
-		newAccountProbability float64
-		maxAddresses          int
-		recipients            []string
-
-		addr    string
-		created bool
-	}{
-		"no addresses": {
-			addresses:             0,
-			randomAmount:          big.NewInt(0),
-			newAccountProbability: 0.0,
-			maxAddresses:          0,
-			recipients:            []string{},
-
-			addr:    "addr 1",
-			created: true,
-		},
-		"less than max addresses - bad flip": {
-			addresses:             100,
-			randomAmount:          big.NewInt(50),
-			newAccountProbability: 0.25,
-			maxAddresses:          200,
-			recipients:            []string{"addr 6", "addr 9"},
-
-			addr:    "addr 6",
-			created: false,
-		},
-		"less than max addresses - good flip": {
-			addresses:             100,
-			randomAmount:          big.NewInt(10),
-			newAccountProbability: 0.25,
-			maxAddresses:          200,
-			recipients:            []string{"addr 6", "addr 8"},
-
-			addr:    "addr 1",
-			created: true,
-		},
-		"max addresses - good flip": {
-			addresses:             100,
-			randomAmount:          big.NewInt(10),
-			newAccountProbability: 0.25,
-			maxAddresses:          100,
-			recipients:            []string{"addr 6", "addr 7"},
-
-			addr:    "addr 6",
-			created: false,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			mockHelper := new(mocks.Helper)
-			constructor.helper = mockHelper
-
-			if test.created {
-				mockHelper.On(
-					"Derive",
-					ctx,
-					constructor.network,
-					mock.Anything,
-					mock.Anything,
-				).Return(
-					"addr 1",
-					nil,
-					nil,
-				)
-				mockHelper.On("StoreKey", ctx, "addr 1", mock.Anything).Return(nil)
-				mockHandler.On("AddressCreated", ctx, "addr 1").Return(nil)
-			}
-
-			addresses := []string{}
-			for i := 0; i < test.addresses; i++ {
-				addresses = append(addresses, fmt.Sprintf("old addr %d", i))
-			}
-
-			mockHelper.On("AllAddresses", ctx).Return(addresses, nil).Once()
-			mockHelper.On(
-				"RandomAmount",
-				big.NewInt(0),
-				big.NewInt(100),
-			).Return(
-				test.randomAmount,
-			).Once()
-			constructor.newAccountProbability = test.newAccountProbability
-			constructor.maxAddresses = test.maxAddresses
-
-			addr, created, err := constructor.canGetNewAddress(ctx, test.recipients)
-			assert.NoError(t, err)
-			assert.Equal(t, test.addr, addr)
-			assert.Equal(t, test.created, created)
-		})
-	}
-}
-
-func TestGenerateScenario_Account(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, _, _ := defaultAccountConstructor(t)
-
-	newAddress := "new addr 1"
-
-	tests := map[string]struct {
-		minimumBalance        *big.Int
-		maximumFee            *big.Int
-		maxAddresses          int
-		newAccountProbability float64
-		randomAccountNumber   *big.Int
-		expectNew             bool
-
-		balances map[string]*big.Int
-
-		senderBalance *big.Int
-
-		sendRandomLowAmount  *big.Int
-		sendAmount           *big.Int
-		sendRandomHighAmount *big.Int
-
-		scenarioCtx *scenario.Context
-		scenarioOps []*types.Operation
-		err         error
-	}{
-		"create new address - good flip": {
-			minimumBalance:        big.NewInt(100),
-			maximumFee:            big.NewInt(10),
-			maxAddresses:          100,
-			newAccountProbability: 0.5,
-			randomAccountNumber:   big.NewInt(1),
-			expectNew:             true,
-
-			balances: map[string]*big.Int{
-				"addr 1": big.NewInt(10),
-				"addr 2": big.NewInt(30),
-				"addr 3": big.NewInt(15),
-				"addr 4": big.NewInt(1000),
-				"addr 5": big.NewInt(2),
-			},
-
-			senderBalance: big.NewInt(1000),
-
-			sendRandomLowAmount:  big.NewInt(100),
-			sendAmount:           big.NewInt(100),
-			sendRandomHighAmount: big.NewInt(890),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(100),
-				Recipient:      newAddress,
-				RecipientValue: big.NewInt(100),
-				Currency:       constructor.currency,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"create new address - bad flip": {
-			minimumBalance:        big.NewInt(100),
-			maximumFee:            big.NewInt(10),
-			maxAddresses:          100,
-			newAccountProbability: 0.5,
-			randomAccountNumber:   big.NewInt(90),
-			expectNew:             false,
-
-			balances: map[string]*big.Int{
-				"addr 1": big.NewInt(10),
-				"addr 2": big.NewInt(30),
-				"addr 3": big.NewInt(15),
-				"addr 4": big.NewInt(1000),
-				"addr 5": big.NewInt(2),
-			},
-
-			senderBalance: big.NewInt(1000),
-
-			sendRandomLowAmount:  big.NewInt(0),
-			sendAmount:           big.NewInt(100),
-			sendRandomHighAmount: big.NewInt(890),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(100),
-				Recipient:      "addr 4",
-				RecipientValue: big.NewInt(100),
-				Currency:       constructor.currency,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"can't afford new address": {
-			minimumBalance:        big.NewInt(500),
-			maximumFee:            big.NewInt(100),
-			maxAddresses:          100,
-			newAccountProbability: 0.5,
-			randomAccountNumber:   big.NewInt(1),
-			expectNew:             false,
-
-			balances: map[string]*big.Int{
-				"addr 1": big.NewInt(10),
-				"addr 2": big.NewInt(30),
-				"addr 3": big.NewInt(15),
-				"addr 4": big.NewInt(1000),
-				"addr 5": big.NewInt(2),
-			},
-
-			senderBalance: big.NewInt(1000),
-
-			sendRandomLowAmount:  big.NewInt(0),
-			sendAmount:           big.NewInt(100),
-			sendRandomHighAmount: big.NewInt(400),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(100),
-				Recipient:      "addr 4",
-				RecipientValue: big.NewInt(100),
-				Currency:       constructor.currency,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"can't afford new address but no addresses above minimum": {
-			minimumBalance:        big.NewInt(500),
-			maximumFee:            big.NewInt(100),
-			maxAddresses:          100,
-			newAccountProbability: 0.5,
-			randomAccountNumber:   big.NewInt(1),
-			expectNew:             false,
-
-			balances: map[string]*big.Int{},
-
-			senderBalance: big.NewInt(1000),
-
-			sendAmount: big.NewInt(100),
-
-			scenarioCtx: nil,
-			scenarioOps: nil,
-			err:         ErrInsufficientFunds,
-		},
-		"zero funds": {
-			minimumBalance:        big.NewInt(0),
-			maximumFee:            big.NewInt(0),
-			maxAddresses:          100,
-			newAccountProbability: 0.5,
-			randomAccountNumber:   big.NewInt(1),
-
-			balances: map[string]*big.Int{
-				"addr 1": big.NewInt(10),
-				"addr 2": big.NewInt(30),
-				"addr 3": big.NewInt(15),
-				"addr 4": big.NewInt(1000),
-				"addr 5": big.NewInt(2),
-			},
-
-			senderBalance: big.NewInt(0),
-
-			sendAmount: big.NewInt(0),
-
-			scenarioCtx: nil,
-			scenarioOps: nil,
-			err:         ErrInsufficientFunds,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			// reset at start of each loop
-			mockHelper := new(mocks.Helper)
-			constructor.helper = mockHelper
-
-			mockHandler := new(mocks.Handler)
-			constructor.handler = mockHandler
-
-			mockHelper.On(
-				"RandomAmount",
-				big.NewInt(0),
-				big.NewInt(100),
-			).Return(
-				test.randomAccountNumber,
-			).Once()
-			if test.expectNew {
-				mockHelper.On(
-					"Derive",
-					ctx,
-					constructor.network,
-					mock.Anything,
-					mock.Anything,
-				).Return(
-					newAddress,
-					nil,
-					nil,
-				).Once()
-				mockHelper.On("StoreKey", ctx, newAddress, mock.Anything).Return(nil)
-				mockHandler.On("AddressCreated", ctx, newAddress).Return(nil).Once()
-			}
-
-			constructor.minimumBalance = test.minimumBalance
-			constructor.maxAddresses = test.maxAddresses
-			constructor.maximumFee = test.maximumFee
-			constructor.newAccountProbability = test.newAccountProbability
-
-			if test.sendRandomLowAmount != nil && test.sendRandomHighAmount != nil {
-				mockHelper.On(
-					"RandomAmount",
-					test.sendRandomLowAmount,
-					test.sendRandomHighAmount,
-				).Return(
-					test.sendAmount,
-				)
-			}
-
-			// Lock in addresses order so don't introduce flaky test (instead of
-			// iterating over balance map)
-			addresses := []string{}
-			for i := 0; i < len(test.balances); i++ {
-				addresses = append(addresses, fmt.Sprintf("addr %d", i+1))
-			}
-
-			mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-
-			for _, k := range addresses {
-				mockHelper.On(
-					"AccountBalance",
-					ctx,
-					&types.AccountIdentifier{Address: k},
-					constructor.currency,
-				).Return(
-					test.balances[k],
-					nil,
-				)
-			}
-
-			scenarioCtx, scenarioOps, err := constructor.generateScenario(
-				ctx,
-				sender,
-				test.senderBalance,
-				nil,
-			)
-			if test.err != nil {
-				assert.Equal(t, test.err, err)
-				assert.Nil(t, scenarioCtx)
-				assert.Nil(t, scenarioOps)
-			} else {
-				assert.NoError(t, err)
-				assert.Equal(t, test.scenarioCtx, scenarioCtx)
-				assert.Equal(t, test.scenarioOps, scenarioOps)
-			}
-		})
-	}
-}
-
-func TestGenerateScenario_Utxo(t *testing.T) {
-	ctx := context.Background()
-
-	constructor, _ := defaultUtxoConstructor(t)
-
-	senderCoin := &types.CoinIdentifier{Identifier: "sender coin"}
-	newAddress := "new addr 1"
-	changeAddress := "change addr 1"
-
-	tests := map[string]struct {
-		minimumBalance        *big.Int
-		maximumFee            *big.Int
-		maxAddresses          int
-		newAccountProbability float64
-
-		recipientAccountNumber *big.Int
-		expectNewRecipient     bool
-
-		changeAccountNumber *big.Int
-		expectNewChange     bool
-
-		changeDifferential       *big.Int
-		changeDifferentialAmount *big.Int
-
-		addresses int
-
-		senderBalance *big.Int
-
-		scenarioCtx *scenario.Context
-		scenarioOps []*types.Operation
-		err         error
-	}{
-		"create new recipient, create new change - good flips": {
-			minimumBalance:         big.NewInt(400),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(1),
-			expectNewRecipient:     true,
-			changeAccountNumber:    big.NewInt(1),
-			expectNewChange:        true,
-
-			changeDifferential:       big.NewInt(300),
-			changeDifferentialAmount: big.NewInt(151),
-
-			addresses: 10,
-
-			senderBalance: big.NewInt(1500),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1500),
-				Recipient:      newAddress,
-				RecipientValue: big.NewInt(551),
-				ChangeAddress:  changeAddress,
-				ChangeValue:    big.NewInt(549),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: append(constructor.scenario, constructor.changeScenario),
-			err:         nil,
-		},
-		"create new recipient, create new change - bad flips": {
-			minimumBalance:         big.NewInt(400),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(95),
-			expectNewRecipient:     true,
-			changeAccountNumber:    big.NewInt(95),
-			expectNewChange:        true,
-
-			changeDifferential:       big.NewInt(300),
-			changeDifferentialAmount: big.NewInt(151),
-
-			addresses: 0,
-
-			senderBalance: big.NewInt(1500),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1500),
-				Recipient:      newAddress,
-				RecipientValue: big.NewInt(551),
-				ChangeAddress:  changeAddress,
-				ChangeValue:    big.NewInt(549),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: append(constructor.scenario, constructor.changeScenario),
-			err:         nil,
-		},
-		"existing recipient, create new change": {
-			minimumBalance:         big.NewInt(400),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(95),
-			expectNewRecipient:     false,
-			changeAccountNumber:    big.NewInt(1),
-			expectNewChange:        true,
-
-			changeDifferential:       big.NewInt(300),
-			changeDifferentialAmount: big.NewInt(151),
-
-			addresses: 10,
-
-			senderBalance: big.NewInt(1500),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1500),
-				Recipient:      "addr 1",
-				RecipientValue: big.NewInt(551),
-				ChangeAddress:  changeAddress,
-				ChangeValue:    big.NewInt(549),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: append(constructor.scenario, constructor.changeScenario),
-			err:         nil,
-		},
-		"existing recipient, existing change": {
-			minimumBalance:         big.NewInt(400),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(95),
-			expectNewRecipient:     false,
-			changeAccountNumber:    big.NewInt(90),
-			expectNewChange:        false,
-
-			changeDifferential:       big.NewInt(300),
-			changeDifferentialAmount: big.NewInt(151),
-
-			addresses: 10,
-
-			senderBalance: big.NewInt(1500),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1500),
-				Recipient:      "addr 1",
-				RecipientValue: big.NewInt(551),
-				ChangeAddress:  "addr 2",
-				ChangeValue:    big.NewInt(549),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: append(constructor.scenario, constructor.changeScenario),
-			err:         nil,
-		},
-		"create new address, no change - good flip": {
-			minimumBalance:         big.NewInt(500),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(1),
-			expectNewRecipient:     true,
-
-			addresses: 10,
-
-			senderBalance: big.NewInt(1000),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1000),
-				Recipient:      newAddress,
-				RecipientValue: big.NewInt(600),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"don't create new address, no change - good flip": {
-			minimumBalance:         big.NewInt(500),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(1),
-			expectNewRecipient:     false,
-
-			addresses: 200,
-
-			senderBalance: big.NewInt(1000),
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1000),
-				Recipient:      "addr 1",
-				RecipientValue: big.NewInt(600),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"create new address, no change - bad flip": {
-			minimumBalance:         big.NewInt(500),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(90),
-			expectNewRecipient:     false,
-
-			senderBalance: big.NewInt(1000),
-
-			addresses: 10,
-
-			scenarioCtx: &scenario.Context{
-				Sender:         sender,
-				SenderValue:    big.NewInt(1000),
-				Recipient:      "addr 1",
-				RecipientValue: big.NewInt(600),
-				Currency:       constructor.currency,
-				CoinIdentifier: senderCoin,
-			},
-			scenarioOps: constructor.scenario,
-			err:         nil,
-		},
-		"insufficient funds": {
-			minimumBalance:         big.NewInt(500),
-			maximumFee:             big.NewInt(400),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(90),
-			expectNewRecipient:     false,
-
-			senderBalance: big.NewInt(500),
-
-			addresses: 10,
-
-			scenarioCtx: nil,
-			scenarioOps: nil,
-			err:         ErrInsufficientFunds,
-		},
-		"zero funds": {
-			minimumBalance:         big.NewInt(0),
-			maximumFee:             big.NewInt(0),
-			maxAddresses:           100,
-			newAccountProbability:  0.5,
-			recipientAccountNumber: big.NewInt(1),
-
-			senderBalance: big.NewInt(0),
-
-			addresses: 10,
-
-			scenarioCtx: nil,
-			scenarioOps: nil,
-			err:         ErrInsufficientFunds,
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			// reset at start of each loop
-			mockHelper := new(mocks.Helper)
-			constructor.helper = mockHelper
-
-			mockHandler := new(mocks.Handler)
-			constructor.handler = mockHandler
-
-			mockHelper.On(
-				"RandomAmount",
-				big.NewInt(0),
-				big.NewInt(100),
-			).Return(
-				test.recipientAccountNumber,
-			).Once()
-			if test.expectNewRecipient {
-				mockHelper.On(
-					"Derive",
-					ctx,
-					constructor.network,
-					mock.Anything,
-					mock.Anything,
-				).Return(
-					newAddress,
-					nil,
-					nil,
-				).Once()
-				mockHelper.On("StoreKey", ctx, newAddress, mock.Anything).Return(nil).Once()
-				mockHandler.On("AddressCreated", ctx, newAddress).Return(nil).Once()
-			}
-
-			mockHelper.On(
-				"RandomAmount",
-				big.NewInt(0),
-				big.NewInt(100),
-			).Return(
-				test.changeAccountNumber,
-			).Once()
-			if test.expectNewChange {
-				mockHelper.On(
-					"Derive",
-					ctx,
-					constructor.network,
-					mock.Anything,
-					mock.Anything,
-				).Return(
-					changeAddress,
-					nil,
-					nil,
-				).Once()
-				mockHelper.On("StoreKey", ctx, changeAddress, mock.Anything).Return(nil).Once()
-				mockHandler.On("AddressCreated", ctx, changeAddress).Return(nil).Once()
-			}
-
-			if test.changeDifferential != nil {
-				mockHelper.On(
-					"RandomAmount",
-					big.NewInt(0),
-					test.changeDifferential,
-				).Return(
-					test.changeDifferentialAmount,
-				).Once()
-			}
-
-			constructor.minimumBalance = test.minimumBalance
-			constructor.maxAddresses = test.maxAddresses
-			constructor.maximumFee = test.maximumFee
-			constructor.newAccountProbability = test.newAccountProbability
-
-			// Lock in addresses order so don't introduce flaky test (instead of
-			// iterating over balance map)
-			addresses := []string{}
-			for i := 0; i < test.addresses; i++ {
-				addresses = append(addresses, fmt.Sprintf("addr %d", i+1))
-			}
-
-			mockHelper.On("AllAddresses", ctx).Return(addresses, nil)
-
-			scenarioCtx, scenarioOps, err := constructor.generateScenario(
-				ctx,
-				sender,
-				test.senderBalance,
-				senderCoin,
-			)
-			if test.err != nil {
-				assert.Equal(t, test.err, err)
-				assert.Nil(t, scenarioCtx)
-				assert.Nil(t, scenarioOps)
-			} else {
-				assert.NoError(t, err)
-				assert.Equal(t, test.scenarioCtx, scenarioCtx)
-				assert.Equal(t, test.scenarioOps, scenarioOps)
-			}
-		})
-	}
-}
diff --git a/pkg/scenario/scenario_test.go b/pkg/scenario/scenario_test.go
deleted file mode 100644
index cf07789..0000000
--- a/pkg/scenario/scenario_test.go
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright 2020 Coinbase, Inc.
-//
-// 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 scenario
-
-import (
-	"context"
-	"math/big"
-	"testing"
-
-	"github.com/coinbase/rosetta-sdk-go/types"
-	"github.com/stretchr/testify/assert"
-)
-
-var (
-	sender         = "addr1"
-	senderValue    = big.NewInt(100)
-	recipient      = "addr2"
-	recipientValue = big.NewInt(70)
-	coinIdentifier = &types.CoinIdentifier{Identifier: "utxo1"}
-	changeAddress  = "addr3"
-	changeValue    = big.NewInt(20)
-
-	bitcoinCurrency = &types.Currency{
-		Symbol:   "BTC",
-		Decimals: 8,
-	}
-	ethereumCurrency = &types.Currency{
-		Symbol:   "ETH",
-		Decimals: 18,
-	}
-)
-
-func TestPopulateScenario(t *testing.T) {
-	var tests = map[string]struct {
-		context  *Context
-		scenario []*types.Operation
-
-		expected []*types.Operation
-	}{
-		"bitcoin": {
-			context: &Context{
-				Sender:         sender,
-				SenderValue:    senderValue,
-				Recipient:      recipient,
-				RecipientValue: recipientValue,
-				CoinIdentifier: coinIdentifier,
-				Currency:       bitcoinCurrency,
-				ChangeAddress:  changeAddress,
-				ChangeValue:    changeValue,
-			},
-			scenario: []*types.Operation{
-				{
-					Type: "Vin",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 0,
-					},
-					Account: &types.AccountIdentifier{
-						Address: "{{ SENDER }}",
-					},
-					Amount: &types.Amount{
-						Value: "{{ SENDER_VALUE }}",
-					},
-					CoinChange: &types.CoinChange{
-						CoinAction: types.CoinSpent,
-						CoinIdentifier: &types.CoinIdentifier{
-							Identifier: "{{ COIN_IDENTIFIER }}",
-						},
-					},
-				},
-				{
-					Type: "Vout",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					Account: &types.AccountIdentifier{
-						Address: "{{ RECIPIENT }}",
-					},
-					Amount: &types.Amount{
-						Value: "{{ RECIPIENT_VALUE }}",
-					},
-				},
-				{
-					Type: "Vout",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 2,
-					},
-					Account: &types.AccountIdentifier{
-						Address: "{{ CHANGE_ADDRESS }}",
-					},
-					Amount: &types.Amount{
-						Value: "{{ CHANGE_VALUE }}",
-					},
-				},
-			},
-			expected: []*types.Operation{
-				{
-					Type: "Vin",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 0,
-					},
-					Account: &types.AccountIdentifier{
-						Address: sender,
-					},
-					Amount: &types.Amount{
-						Value:    new(big.Int).Neg(senderValue).String(),
-						Currency: bitcoinCurrency,
-					},
-					CoinChange: &types.CoinChange{
-						CoinAction:     types.CoinSpent,
-						CoinIdentifier: coinIdentifier,
-					},
-				},
-				{
-					Type: "Vout",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					Account: &types.AccountIdentifier{
-						Address: recipient,
-					},
-					Amount: &types.Amount{
-						Value:    new(big.Int).Abs(recipientValue).String(),
-						Currency: bitcoinCurrency,
-					},
-				},
-				{
-					Type: "Vout",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 2,
-					},
-					Account: &types.AccountIdentifier{
-						Address: changeAddress,
-					},
-					Amount: &types.Amount{
-						Value:    new(big.Int).Abs(changeValue).String(),
-						Currency: bitcoinCurrency,
-					},
-				},
-			},
-		},
-		"ethereum": {
-			context: &Context{
-				Sender:         sender,
-				SenderValue:    senderValue,
-				Recipient:      recipient,
-				RecipientValue: recipientValue,
-				Currency:       ethereumCurrency,
-			},
-			scenario: []*types.Operation{
-				{
-					Type: "transfer",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 0,
-					},
-					Account: &types.AccountIdentifier{
-						Address: "{{ SENDER }}",
-					},
-					Amount: &types.Amount{
-						Value: "{{ SENDER_VALUE }}",
-					},
-				},
-				{
-					Type: "transfer",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					RelatedOperations: []*types.OperationIdentifier{
-						{
-							Index: 0,
-						},
-					},
-					Account: &types.AccountIdentifier{
-						Address: "{{ RECIPIENT }}",
-					},
-					Amount: &types.Amount{
-						Value: "{{ RECIPIENT_VALUE }}",
-					},
-				},
-			},
-			expected: []*types.Operation{
-				{
-					Type: "transfer",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 0,
-					},
-					Account: &types.AccountIdentifier{
-						Address: sender,
-					},
-					Amount: &types.Amount{
-						Value:    new(big.Int).Neg(senderValue).String(),
-						Currency: ethereumCurrency,
-					},
-				},
-				{
-					Type: "transfer",
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					RelatedOperations: []*types.OperationIdentifier{
-						{
-							Index: 0,
-						},
-					},
-					Account: &types.AccountIdentifier{
-						Address: recipient,
-					},
-					Amount: &types.Amount{
-						Value:    new(big.Int).Abs(recipientValue).String(),
-						Currency: ethereumCurrency,
-					},
-				},
-			},
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			ctx := context.Background()
-
-			ops, err := PopulateScenario(
-				ctx,
-				test.context,
-				test.scenario,
-			)
-			assert.NoError(t, err)
-			assert.ElementsMatch(t, test.expected, ops)
-		})
-	}
-}
diff --git a/pkg/tester/construction.go b/pkg/tester/construction.go
index 891d0bd..9ed2bf2 100644
--- a/pkg/tester/construction.go
+++ b/pkg/tester/construction.go
@@ -22,10 +22,10 @@ import (
 	"time"
 
 	"github.com/coinbase/rosetta-cli/configuration"
-	"github.com/coinbase/rosetta-cli/pkg/constructor"
 	"github.com/coinbase/rosetta-cli/pkg/logger"
 	"github.com/coinbase/rosetta-cli/pkg/processor"
 
+	"github.com/coinbase/rosetta-sdk-go/constructor/coordinator"
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
 	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/statefulsyncer"
@@ -49,9 +49,9 @@ type ConstructionTester struct {
 	syncer           *statefulsyncer.StatefulSyncer
 	logger           *logger.Logger
 	onlineFetcher    *fetcher.Fetcher
-	constructor      *constructor.Constructor
 	broadcastStorage *storage.BroadcastStorage
 	blockStorage     *storage.BlockStorage
+	coordinator      *coordinator.Coordinator
 }
 
 // InitializeConstruction initiates the construction API tester.
@@ -112,7 +112,6 @@ func InitializeConstruction(
 
 	broadcastStorage := storage.NewBroadcastStorage(
 		localStore,
-		config.Construction.ConfirmationDepth,
 		config.Construction.StaleDepth,
 		config.Construction.BroadcastLimit,
 		config.TipDelay,
@@ -120,20 +119,11 @@ func InitializeConstruction(
 		config.Construction.BlockBroadcastLimit,
 	)
 
+	parser := parser.New(onlineFetcher.Asserter, nil)
 	broadcastHelper := processor.NewBroadcastStorageHelper(
-		network,
 		blockStorage,
 		onlineFetcher,
 	)
-	parser := parser.New(onlineFetcher.Asserter, nil)
-	broadcastHandler := processor.NewBroadcastStorageHandler(
-		config,
-		counterStorage,
-		parser,
-	)
-
-	broadcastStorage.Initialize(broadcastHelper, broadcastHandler)
-
 	offlineFetcher := fetcher.New(
 		config.Construction.OfflineURL,
 		fetcher.WithAsserter(onlineFetcher.Asserter),
@@ -168,7 +158,7 @@ func InitializeConstruction(
 				Address: address,
 			},
 			Network:  network,
-			Currency: config.Construction.Currency,
+			Currency: prefundedAcc.Currency,
 		}
 
 		accountBalanceRequests = append(accountBalanceRequests, accountBalance)
@@ -189,31 +179,43 @@ func InitializeConstruction(
 		return nil, fmt.Errorf("%w: unable to set coin balances", err)
 	}
 
-	constructorHelper := processor.NewConstructorHelper(
+	jobStorage := storage.NewJobStorage(localStore)
+	coordinatorHelper := processor.NewCoordinatorHelper(
 		offlineFetcher,
 		onlineFetcher,
+		localStore,
 		blockStorage,
 		keyStorage,
 		balanceStorage,
 		coinStorage,
 		broadcastStorage,
-	)
-
-	constructorHandler := processor.NewConstructorHandler(
 		balanceStorageHelper,
 		counterStorage,
 	)
 
-	constructor, err := constructor.New(
-		config,
+	coordinatorHandler := processor.NewCoordinatorHandler(
+		counterStorage,
+	)
+	coordinator, err := coordinator.New(
+		jobStorage,
+		coordinatorHelper,
+		coordinatorHandler,
 		parser,
-		constructorHelper,
-		constructorHandler,
+		config.Construction.Workflows,
 	)
 	if err != nil {
-		return nil, fmt.Errorf("%w: unable to create constructor", err)
+		log.Fatalf("%s: unable to create coordinator", err.Error())
 	}
 
+	broadcastHandler := processor.NewBroadcastStorageHandler(
+		config,
+		counterStorage,
+		coordinator,
+		parser,
+	)
+
+	broadcastStorage.Initialize(broadcastHelper, broadcastHandler)
+
 	syncer := statefulsyncer.New(
 		ctx,
 		network,
@@ -232,7 +234,7 @@ func InitializeConstruction(
 		config:           config,
 		syncer:           syncer,
 		logger:           logger,
-		constructor:      constructor,
+		coordinator:      coordinator,
 		broadcastStorage: broadcastStorage,
 		blockStorage:     blockStorage,
 		onlineFetcher:    onlineFetcher,
@@ -300,10 +302,16 @@ func (t *ConstructionTester) StartSyncer(
 func (t *ConstructionTester) StartConstructor(
 	ctx context.Context,
 ) error {
-	return t.constructor.CreateTransactions(
-		ctx,
-		t.config.Construction.ClearBroadcasts,
-	)
+	if t.config.Construction.ClearBroadcasts {
+		broadcasts, err := t.broadcastStorage.ClearBroadcasts(ctx)
+		if err != nil {
+			return fmt.Errorf("%w: unable to clear broadcasts", err)
+		}
+
+		log.Printf("cleared %d broadcasts\n", len(broadcasts))
+	}
+
+	return t.coordinator.Process(ctx)
 }
 
 // PerformBroadcasts attempts to rebroadcast all pending transactions