Repository: algorand/indexer
Pull request #514 (2021-05-26 19:11:15)

DO NOT MERGE - indexer 2.4.0 release branch
<!--
Thanks for submitting a pull request! We appreciate the time and effort you spent to get this far.

If you haven't already, please make sure that you've reviewed the CONTRIBUTING guide:
https://github.com/algorand/go-algorand/blob/master/CONTRIBUTING.md#code-guidelines
-->

## Summary

Explain the goal of this change and what problem it is solving. Format this cleanly so that it may be used for a commit message, as your changes will be squash-merged.

## Test Plan

How did you test these changes? Please provide the exact scenarios you tested in as much detail as possible including commands, output and rationale.



Patch:
diff --git a/.travis.yml b/.travis.yml
index 63e18ee..473d925 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,8 +17,9 @@ addons:
     - python3-setuptools
     - python3-wheel
 script:
+- test -z `go fmt ./...`
 - make lint
-- make
+- make check
 - make integration
 - make test
 # e2e tests can't run without installing algod.
diff --git a/.version b/.version
index 3f684d2..197c4d5 100644
--- a/.version
+++ b/.version
@@ -1,1 +1,1 @@
-2.3.4
+2.4.0
diff --git a/Makefile b/Makefile
index 520dc7b..6b00be9 100644
--- a/Makefile
+++ b/Makefile
@@ -12,19 +12,23 @@ GOLDFLAGS += -X github.com/algorand/indexer/version.GitDecorateBase64=$(shell gi
 GOLDFLAGS += -X github.com/algorand/indexer/version.ReleaseVersion=$(shell cat .version)
 
 # This is the default target, build the indexer:
-cmd/algorand-indexer/algorand-indexer:	idb/postgres/setup_postgres_sql.go types/protocols_json.go
+cmd/algorand-indexer/algorand-indexer:	idb/postgres/setup_postgres_sql.go idb/postgres/reset_sql.go types/protocols_json.go
 	cd cmd/algorand-indexer && CGO_ENABLED=0 go build -ldflags="${GOLDFLAGS}"
 
-idb/postgres/setup_postgres_sql.go:	idb/postgres/setup_postgres.sql
+idb/postgres/setup_postgres_sql.go idb/postgres/reset_sql.go:	idb/postgres/setup_postgres.sql idb/postgres/reset.sql
 	cd idb/postgres && go generate
 
 types/protocols_json.go:	types/protocols.json types/consensus.go
 	cd types && go generate
 
-idb/mocks/IndexerDb.go:	idb/dummy.go
+idb/mocks/IndexerDb.go:	idb/idb.go
 	go get github.com/vektra/mockery/.../
 	cd idb && mockery -name=IndexerDb
 
+# check that all packages (except tests) compile
+check:
+	go build ./...
+
 package:
 	rm -rf $(PKG_DIR)
 	mkdir -p $(PKG_DIR)
@@ -68,4 +72,4 @@ sign:
 test-package:
 	mule/e2e.sh
 
-.PHONY: test e2e integration fmt lint deploy sign test-package package fakepackage cmd/algorand-indexer/algorand-indexer
+.PHONY: test e2e integration fmt lint deploy sign test-package package fakepackage cmd/algorand-indexer/algorand-indexer idb/mocks/IndexerDb.go
diff --git a/accounting/accounting.go b/accounting/accounting.go
index 4d110cb..4997056 100644
--- a/accounting/accounting.go
+++ b/accounting/accounting.go
@@ -1,13 +1,12 @@
 package accounting
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 	"math/big"
 
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/types"
@@ -26,10 +25,6 @@ type State struct {
 
 	rewardsLevel uint64
 
-	// number of txns at the end of the previous block
-	txnCounter      uint64
-	txnCounterRound uint64
-
 	accountTypes accountTypeCache
 }
 
@@ -46,7 +41,7 @@ func (accounting *State) InitRound(block types.Block) error {
 }
 
 // InitRoundParts are the specific parts from a block needed to initialize accounting. Used for testing, normally you would pass in the block.
-func (accounting *State) InitRoundParts(round uint64, feeSink, rewardsPool atypes.Address, rewardsLevel uint64) error {
+func (accounting *State) InitRoundParts(round uint64, feeSink, rewardsPool sdk_types.Address, rewardsLevel uint64) error {
 	accounting.RoundUpdates.Clear()
 	accounting.feeAddr = feeSink
 	accounting.rewardAddr = rewardsPool
@@ -55,12 +50,6 @@ func (accounting *State) InitRoundParts(round uint64, feeSink, rewardsPool atype
 	return nil
 }
 
-var zeroAddr = [32]byte{}
-
-func addrIsZero(a types.Address) bool {
-	return bytes.Equal(a[:], zeroAddr[:])
-}
-
 func bytesAreZero(b []byte) bool {
 	for _, x := range b {
 		if x != 0 {
@@ -188,7 +177,7 @@ func (accounting *State) addAssetAccounting(addr types.Address, update idb.Asset
 	}
 }
 
-func (accounting *State) configAsset(assetID uint64, isNew bool, creator types.Address, params atypes.AssetParams) {
+func (accounting *State) configAsset(assetID uint64, isNew bool, creator types.Address, params sdk_types.AssetParams) {
 	update := idb.AssetUpdate{
 		AssetID:       assetID,
 		DefaultFrozen: accounting.defaultFrozen[assetID],
@@ -230,7 +219,7 @@ func (accounting *State) destroyAsset(assetID uint64) {
 }
 
 // TODO: move to go-algorand-sdk as Signature.IsZero()
-func zeroSig(sig atypes.Signature) bool {
+func zeroSig(sig sdk_types.Signature) bool {
 	for _, b := range sig {
 		if b != 0 {
 			return false
@@ -240,7 +229,7 @@ func zeroSig(sig atypes.Signature) bool {
 }
 
 // TODO: move to go-algorand-sdk as LogicSig.Blank()
-func blankLsig(lsig atypes.LogicSig) bool {
+func blankLsig(lsig sdk_types.LogicSig) bool {
 	return len(lsig.Logic) == 0
 }
 
@@ -301,7 +290,7 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 	}
 
 	switch stxn.Txn.Type {
-	case atypes.PaymentTx:
+	case sdk_types.PaymentTx:
 		amount := int64(stxn.Txn.Amount)
 		if amount != 0 {
 			accounting.updateAlgo(stxn.Txn.Sender, -amount)
@@ -322,7 +311,7 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 		if AccountCloseTxn(stxn.Txn.Sender, stxn) {
 			accounting.closeAccount(stxn.Txn.Sender)
 		}
-	case atypes.KeyRegistrationTx:
+	case sdk_types.KeyRegistrationTx:
 		// see https://github.com/algorand/go-algorand/blob/master/data/transactions/keyreg.go
 		accounting.updateAccountData(stxn.Txn.Sender, "vote", stxn.Txn.VotePK)
 		accounting.updateAccountData(stxn.Txn.Sender, "sel", stxn.Txn.SelectionPK)
@@ -341,7 +330,7 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 			accounting.updateAccountData(stxn.Txn.Sender, "voteLst", uint64(stxn.Txn.VoteLast))
 			accounting.updateAccountData(stxn.Txn.Sender, "voteKD", stxn.Txn.VoteKeyDilution)
 		}
-	case atypes.AssetConfigTx:
+	case sdk_types.AssetConfigTx:
 		assetID := uint64(stxn.Txn.ConfigAsset)
 		isNew := AssetCreateTxn(stxn)
 		if isNew {
@@ -366,7 +355,7 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 				accounting.updateAsset(stxn.Txn.Sender, assetID, stxn.Txn.AssetParams.Total, 0, false)
 			}
 		}
-	case atypes.AssetTransferTx:
+	case sdk_types.AssetTransferTx:
 		assetID := uint64(stxn.Txn.XferAsset)
 		defaultFrozen := accounting.defaultFrozen[assetID]
 		sender := stxn.Txn.AssetSender // clawback
@@ -384,10 +373,10 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 		if AssetOptOutTxn(stxn) {
 			accounting.closeAsset(sender, assetID, stxn.Txn.AssetCloseTo, round, intra)
 		}
-	case atypes.AssetFreezeTx:
+	case sdk_types.AssetFreezeTx:
 		accounting.freezeAsset(stxn.Txn.FreezeAccount, uint64(stxn.Txn.FreezeAsset), stxn.Txn.AssetFrozen)
-	case atypes.ApplicationCallTx:
-		hasGlobal := (len(stxn.EvalDelta.GlobalDelta) > 0) || (len(stxn.Txn.ApprovalProgram) > 0) || (len(stxn.Txn.ClearStateProgram) > 0) || stxn.Txn.OnCompletion == atypes.DeleteApplicationOC
+	case sdk_types.ApplicationCallTx:
+		hasGlobal := (len(stxn.EvalDelta.GlobalDelta) > 0) || (len(stxn.Txn.ApprovalProgram) > 0) || (len(stxn.Txn.ClearStateProgram) > 0) || stxn.Txn.OnCompletion == sdk_types.DeleteApplicationOC
 		appid := uint64(stxn.Txn.ApplicationID)
 		if appid == 0 {
 			// creation
@@ -437,7 +426,7 @@ func (accounting *State) AddTransaction(txnr *idb.TxnRow) (err error) {
 			)
 		}
 		// if there's no other content change, but a state change of opt-in/close-out/clear-state, record that
-		if len(stxn.EvalDelta.LocalDeltas) == 0 && (stxn.Txn.OnCompletion == atypes.OptInOC || stxn.Txn.OnCompletion == atypes.CloseOutOC || stxn.Txn.OnCompletion == atypes.ClearStateOC) {
+		if len(stxn.EvalDelta.LocalDeltas) == 0 && (stxn.Txn.OnCompletion == sdk_types.OptInOC || stxn.Txn.OnCompletion == sdk_types.CloseOutOC || stxn.Txn.OnCompletion == sdk_types.ClearStateOC) {
 			accounting.AppLocalDeltas = append(
 				accounting.AppLocalDeltas,
 				idb.AppDelta{
diff --git a/accounting/rewind.go b/accounting/rewind.go
index 09246b2..8e8bfa0 100644
--- a/accounting/rewind.go
+++ b/accounting/rewind.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 	models "github.com/algorand/indexer/api/generated/v2"
 
 	"github.com/algorand/indexer/idb"
@@ -78,7 +78,7 @@ func AccountAtRound(account models.Account, round uint64, db idb.IndexerDb) (acc
 
 	acct = account
 	var addr types.Address
-	addr, err = atypes.DecodeAddress(account.Address)
+	addr, err = sdk_types.DecodeAddress(account.Address)
 	if err != nil {
 		return
 	}
@@ -122,7 +122,7 @@ func AccountAtRound(account models.Account, round uint64, db idb.IndexerDb) (acc
 			acct.Rewards -= uint64(stxn.SenderRewards)
 		}
 		switch stxn.Txn.Type {
-		case atypes.PaymentTx:
+		case sdk_types.PaymentTx:
 			if addr == stxn.Txn.Sender {
 				acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Amount)
 			}
@@ -140,14 +140,14 @@ func AccountAtRound(account models.Account, round uint64, db idb.IndexerDb) (acc
 				// unwind sending a close-to
 				acct.AmountWithoutPendingRewards += uint64(stxn.ClosingAmount)
 			}
-		case atypes.KeyRegistrationTx:
+		case sdk_types.KeyRegistrationTx:
 			// TODO: keyreg does not rewind. workaround: query for txns on an account with typeenum=2 to find previous values it was set to.
-		case atypes.AssetConfigTx:
+		case sdk_types.AssetConfigTx:
 			if stxn.Txn.ConfigAsset == 0 {
 				// create asset, unwind the application of the value
 				assetUpdate(&acct, txnrow.AssetID, 0, stxn.Txn.AssetParams.Total)
 			}
-		case atypes.AssetTransferTx:
+		case sdk_types.AssetTransferTx:
 			if addr == stxn.Txn.AssetSender || addr == stxn.Txn.Sender {
 				assetUpdate(&acct, uint64(stxn.Txn.XferAsset), stxn.Txn.AssetAmount+txnrow.Extra.AssetCloseAmount, 0)
 			}
@@ -157,7 +157,7 @@ func AccountAtRound(account models.Account, round uint64, db idb.IndexerDb) (acc
 			if addr == stxn.Txn.AssetCloseTo {
 				assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, txnrow.Extra.AssetCloseAmount)
 			}
-		case atypes.AssetFreezeTx:
+		case sdk_types.AssetFreezeTx:
 		default:
 			err = fmt.Errorf("%s[%d,%d]: rewinding past txn type %s is not currently supported", account.Address, txnrow.Round, txnrow.Intra, stxn.Txn.Type)
 			return
@@ -205,7 +205,7 @@ func AccountAtRound(account models.Account, round uint64, db idb.IndexerDb) (acc
 				return
 			}
 			var proto types.ConsensusParams
-			proto, err = db.GetProto(string(blockheader.CurrentProtocol))
+			proto, err = types.Protocol(string(blockheader.CurrentProtocol))
 			if err != nil {
 				return
 			}
diff --git a/api/converter_utils.go b/api/converter_utils.go
index a85d65b..89e4bbb 100644
--- a/api/converter_utils.go
+++ b/api/converter_utils.go
@@ -292,7 +292,7 @@ func txnRowToTransaction(row idb.TxnRow) (generated.Transaction, error) {
 	switch stxn.Txn.Type {
 	case sdk_types.PaymentTx:
 		p := generated.TransactionPayment{
-			CloseAmount:      uint64Ptr(row.Extra.AssetCloseAmount),
+			CloseAmount:      uint64Ptr(uint64(stxn.ApplyData.ClosingAmount)),
 			CloseRemainderTo: addrPtr(stxn.Txn.CloseRemainderTo),
 			Receiver:         stxn.Txn.Receiver.String(),
 			Amount:           uint64(stxn.Txn.Amount),
@@ -528,11 +528,11 @@ func transactionParamsToTransactionFilter(params generated.SearchForTransactions
 
 	// filter Algos or Asset but not both.
 	if filter.AssetID != 0 {
-		filter.AssetAmountLT = uintOrDefault(params.CurrencyLessThan)
-		filter.AssetAmountGT = uintOrDefault(params.CurrencyGreaterThan)
+		filter.AssetAmountLT = params.CurrencyLessThan
+		filter.AssetAmountGT = params.CurrencyGreaterThan
 	} else {
-		filter.AlgosLT = uintOrDefault(params.CurrencyLessThan)
-		filter.AlgosGT = uintOrDefault(params.CurrencyGreaterThan)
+		filter.AlgosLT = params.CurrencyLessThan
+		filter.AlgosGT = params.CurrencyGreaterThan
 	}
 	filter.Round = params.Round
 
diff --git a/api/handlers.go b/api/handlers.go
index 66a9509..521b51c 100644
--- a/api/handlers.go
+++ b/api/handlers.go
@@ -7,9 +7,8 @@ import (
 	"strconv"
 
 	"github.com/labstack/echo/v4"
-	log "github.com/sirupsen/logrus"
 
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/accounting"
 	"github.com/algorand/indexer/api/generated/common"
@@ -29,8 +28,6 @@ type ServerImplementation struct {
 	db idb.IndexerDb
 
 	fetcher error
-
-	log *log.Logger
 }
 
 /////////////////////
@@ -155,15 +152,15 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener
 
 	// Set GT/LT on Algos or Asset depending on whether or not an assetID was specified
 	if options.HasAssetID == 0 {
-		options.AlgosGreaterThan = uintOrDefault(params.CurrencyGreaterThan)
-		options.AlgosLessThan = uintOrDefault(params.CurrencyLessThan)
+		options.AlgosGreaterThan = params.CurrencyGreaterThan
+		options.AlgosLessThan = params.CurrencyLessThan
 	} else {
-		options.AssetGT = uintOrDefault(params.CurrencyGreaterThan)
-		options.AssetLT = uintOrDefault(params.CurrencyLessThan)
+		options.AssetGT = params.CurrencyGreaterThan
+		options.AssetLT = params.CurrencyLessThan
 	}
 
 	if params.Next != nil {
-		addr, err := types.DecodeAddress(*params.Next)
+		addr, err := sdk_types.DecodeAddress(*params.Next)
 		if err != nil {
 			ctx.JSON(http.StatusBadRequest, errUnableToParseNext)
 		}
@@ -308,14 +305,14 @@ func (si *ServerImplementation) LookupAssetByID(ctx echo.Context, assetID uint64
 func (si *ServerImplementation) LookupAssetBalances(ctx echo.Context, assetID uint64, params generated.LookupAssetBalancesParams) error {
 	query := idb.AssetBalanceQuery{
 		AssetID:        assetID,
-		AmountGT:       uintOrDefault(params.CurrencyGreaterThan),
-		AmountLT:       uintOrDefault(params.CurrencyLessThan),
+		AmountGT:       params.CurrencyGreaterThan,
+		AmountLT:       params.CurrencyLessThan,
 		IncludeDeleted: boolOrDefault(params.IncludeAll),
 		Limit:          min(uintOrDefaultValue(params.Limit, defaultBalancesLimit), maxBalancesLimit),
 	}
 
 	if params.Next != nil {
-		addr, err := types.DecodeAddress(*params.Next)
+		addr, err := sdk_types.DecodeAddress(*params.Next)
 		if err != nil {
 			ctx.JSON(http.StatusBadRequest, errUnableToParseNext)
 		}
@@ -495,7 +492,7 @@ func (si *ServerImplementation) fetchAssets(ctx context.Context, options idb.Ass
 			return nil, round, row.Error
 		}
 
-		creator := types.Address{}
+		creator := sdk_types.Address{}
 		if len(row.Creator) != len(creator) {
 			return nil, round, fmt.Errorf(errInvalidCreatorAddress)
 		}
@@ -540,7 +537,7 @@ func (si *ServerImplementation) fetchAssetBalances(ctx context.Context, options
 			return nil, round, row.Error
 		}
 
-		addr := types.Address{}
+		addr := sdk_types.Address{}
 		if len(row.Address) != len(addr) {
 			return nil, round, fmt.Errorf(errInvalidCreatorAddress)
 		}
@@ -627,6 +624,11 @@ func (si *ServerImplementation) fetchBlock(ctx context.Context, round uint64) (g
 func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.AccountQueryOptions, atRound *uint64) ([]generated.Account, uint64 /*round*/, error) {
 	accountchan, round := si.db.GetAccounts(ctx, options)
 
+	if (atRound != nil) && (*atRound > round) {
+		return nil, round, fmt.Errorf(
+			"%s: the requested round %d > the current round %d", errRewindingAccount, *atRound, round)
+	}
+
 	accounts := make([]generated.Account, 0)
 	for row := range accountchan {
 		if row.Error != nil {
@@ -645,7 +647,7 @@ func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.A
 
 		// Compute for a given round if requested.
 		var account generated.Account
-		if (atRound != nil) && (*atRound < row.Account.Round) {
+		if atRound != nil {
 			acct, err := accounting.AccountAtRound(row.Account, *atRound, si.db)
 			if err != nil {
 				// Ignore the error if this is an account search rewind error
@@ -697,13 +699,6 @@ func min(x, y uint64) uint64 {
 	return y
 }
 
-func max(x, y uint64) uint64 {
-	if x > y {
-		return x
-	}
-	return y
-}
-
 // isSpecialAccount returns true if addr belongs to a special account, false otherwise.
 // The function returns an error in case it fails to retrieve the special accounts list.
 func (si *ServerImplementation) isSpecialAccount(addr string) (bool, error) {
diff --git a/api/pointer_utils.go b/api/pointer_utils.go
index 50a594d..51b047c 100644
--- a/api/pointer_utils.go
+++ b/api/pointer_utils.go
@@ -3,7 +3,7 @@ package api
 import (
 	"time"
 
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 )
 
 ////////////////////////////////
@@ -66,7 +66,7 @@ func timePtr(x time.Time) *time.Time {
 	return &x
 }
 
-func addrPtr(x types.Address) *string {
+func addrPtr(x sdk_types.Address) *string {
 	if x.IsZero() {
 		return nil
 	}
@@ -92,8 +92,3 @@ func strArrayPtr(x []string) *[]string {
 	}
 	return &x
 }
-
-type genesis struct {
-	genesisHash []byte
-	genesisID   string
-}
diff --git a/api/server.go b/api/server.go
index 5eec0da..7547c66 100644
--- a/api/server.go
+++ b/api/server.go
@@ -16,13 +16,8 @@ import (
 	"github.com/algorand/indexer/idb"
 )
 
-// TODO: Get rid of this global
-var indexerDb idb.IndexerDb
-
 // Serve starts an http server for the indexer API. This call blocks.
 func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, fetcherError error, log *log.Logger, tokens []string, developerMode bool) {
-	indexerDb = db
-
 	e := echo.New()
 	e.HideBanner = true
 
diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go
index 046362d..f5bf0dc 100644
--- a/cmd/algorand-indexer/daemon.go
+++ b/cmd/algorand-indexer/daemon.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"path/filepath"
 	"time"
 
 	"github.com/spf13/cobra"
@@ -25,6 +24,7 @@ var (
 	daemonServerAddr string
 	noAlgod          bool
 	developerMode    bool
+	allowMigration   bool
 	tokenString      string
 )
 
@@ -46,11 +46,6 @@ var daemonCmd = &cobra.Command{
 		if algodDataDir == "" {
 			algodDataDir = os.Getenv("ALGORAND_DATA")
 		}
-		opts := idb.IndexerDbOptions{}
-		if noAlgod {
-			opts.ReadOnly = true
-		}
-		db := globalIndexerDb(&opts)
 
 		ctx, cf := context.WithCancel(context.Background())
 		defer cf()
@@ -61,27 +56,24 @@ var daemonCmd = &cobra.Command{
 			bot, err = fetcher.ForNetAndToken(algodAddr, algodToken, logger)
 			maybeFail(err, "fetcher setup, %v", err)
 		} else if algodDataDir != "" {
-			if genesisJSONPath == "" {
-				genesisJSONPath = filepath.Join(algodDataDir, "genesis.json")
-			}
 			bot, err = fetcher.ForDataDir(algodDataDir, logger)
 			maybeFail(err, "fetcher setup, %v", err)
 		} else {
 			// no algod was found
 			noAlgod = true
 		}
-		if !noAlgod {
-			// Only do this if we're going to be writing
-			// to the db, to allow for read-only query
-			// servers that hit the db backend.
-			err := importer.ImportProto(db)
-			maybeFail(err, "import proto, %v", err)
+		opts := idb.IndexerDbOptions{}
+		if noAlgod && !allowMigration {
+			opts.ReadOnly = true
 		}
+		db := indexerDbFromFlags(opts)
 		if bot != nil {
 			logger.Info("Initializing block import handler.")
 			maxRound, err := db.GetMaxRoundLoaded()
-			maybeFail(err, "failed to get max round, %v", err)
-			if maxRound != 0 {
+			if err == idb.ErrorNotInitialized {
+				bot.SetNextRound(0)
+			} else {
+				maybeFail(err, "failed to get max round, %v", err)
 				bot.SetNextRound(maxRound + 1)
 			}
 			cache, err := db.GetDefaultFrozen()
@@ -90,12 +82,15 @@ var daemonCmd = &cobra.Command{
 				imp:   importer.NewDBImporter(db),
 				db:    db,
 				cache: cache,
-				round: maxRound,
 			}
 			bot.AddBlockHandler(&bih)
 			bot.SetContext(ctx)
 			go func() {
 				waitForDBAvailable(db)
+
+				// Initial import if needed.
+				importer.InitialImport(db, genesisJSONPath, bot.Algod(), logger)
+
 				logger.Info("Starting block importer.")
 				bot.Run()
 				cf()
@@ -154,6 +149,7 @@ func init() {
 	daemonCmd.Flags().BoolVarP(&noAlgod, "no-algod", "", false, "disable connecting to algod for block following")
 	daemonCmd.Flags().StringVarP(&tokenString, "token", "t", "", "an optional auth token, when set REST calls must use this token in a bearer format, or in a 'X-Indexer-API-Token' header")
 	daemonCmd.Flags().BoolVarP(&developerMode, "dev-mode", "", false, "allow performance intensive operations like searching for accounts at a particular round")
+	daemonCmd.Flags().BoolVarP(&allowMigration, "allow-migration", "", false, "allow migrations to happen even when no algod connected")
 
 	viper.RegisterAlias("algod", "algod-data-dir")
 	viper.RegisterAlias("algod-net", "algod-address")
@@ -165,18 +161,27 @@ type blockImporterHandler struct {
 	imp   importer.Importer
 	db    idb.IndexerDb
 	cache map[uint64]bool
-	round uint64
 }
 
 func (bih *blockImporterHandler) HandleBlock(block *types.EncodedBlockCert) {
 	start := time.Now()
-	if uint64(block.Block.Round) != bih.round+1 {
-		logger.Errorf("received block %d when expecting %d", block.Block.Round, bih.round+1)
-	}
 	_, err := bih.imp.ImportDecodedBlock(block)
-	maybeFail(err, "ImportDecodedBlock %d: %v", block.Block.Round, err)
-	importer.UpdateAccounting(bih.db, bih.cache, uint64(block.Block.Round), genesisJSONPath, logger)
+	maybeFail(err, "ImportDecodedBlock %d", block.Block.Round)
+	maxRoundAccounted, err := bih.db.GetMaxRoundAccounted()
+	var nextUnaccountedRound uint64
+	// Special case to start at round 0 if things are uninitialized, otherwise start at the first unaccounted round.
+	if err == idb.ErrorNotInitialized {
+		nextUnaccountedRound = 0
+	} else {
+		maybeFail(err, "failed to get max round accounted.")
+		nextUnaccountedRound = maxRoundAccounted + 1
+	}
+	// During normal operation StartRound and MaxRound will be the same round.
+	filter := idb.UpdateFilter{
+		StartRound: nextUnaccountedRound,
+		MaxRound:   uint64(block.Block.Round),
+	}
+	importer.UpdateAccounting(bih.db, bih.cache, filter, logger)
 	dt := time.Now().Sub(start)
 	logger.Infof("round r=%d (%d txn) imported in %s", block.Block.Round, len(block.Block.Payset), dt.String())
-	bih.round = uint64(block.Block.Round)
 }
diff --git a/cmd/algorand-indexer/import.go b/cmd/algorand-indexer/import.go
index d9776d5..7d8f1bd 100644
--- a/cmd/algorand-indexer/import.go
+++ b/cmd/algorand-indexer/import.go
@@ -7,6 +7,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/algorand/indexer/config"
+	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/importer"
 )
 
@@ -22,7 +23,7 @@ var importCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		db := globalIndexerDb(nil)
+		db := indexerDbFromFlags(idb.IndexerDbOptions{})
 
 		cache, err := db.GetDefaultFrozen()
 		if err != nil {
diff --git a/cmd/algorand-indexer/main.go b/cmd/algorand-indexer/main.go
index 92c1d76..b9756d3 100644
--- a/cmd/algorand-indexer/main.go
+++ b/cmd/algorand-indexer/main.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/algorand/indexer/config"
 	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/idb/dummy"
 	_ "github.com/algorand/indexer/idb/postgres"
 	"github.com/algorand/indexer/version"
 )
@@ -22,14 +23,14 @@ func maybeFail(err error, errfmt string, params ...interface{}) {
 	if err == nil {
 		return
 	}
-	logger.Errorf(errfmt, params...)
+	logger.WithError(err).Errorf(errfmt, params...)
 	os.Exit(1)
 }
 
 var rootCmd = &cobra.Command{
 	Use:   "indexer",
 	Short: "Algorand Indexer",
-	Long:  `indexer imports blocks from an algod node or from local files into an SQL database for querying. indexer is a daemon that can serve queries from that database.`,
+	Long:  `Indexer imports blocks from an algod node into an SQL database for querying. It is a daemon that can serve queries from that database.`,
 	Args:  cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		//If no arguments passed, we should fallback to help
@@ -77,25 +78,22 @@ var (
 	doVersion      bool
 	cpuProfile     string
 	pidFilePath    string
-	db             idb.IndexerDb
 	profFile       io.WriteCloser
 	logLevel       string
 	logFile        string
 	logger         *log.Logger
 )
 
-func globalIndexerDb(opts *idb.IndexerDbOptions) idb.IndexerDb {
-	if db == nil {
-		if postgresAddr != "" {
-			var err error
-			db, err = idb.IndexerDbByName("postgres", postgresAddr, opts, logger)
-			maybeFail(err, "could not init db, %v", err)
-		} else if dummyIndexerDb {
-			db = idb.DummyIndexerDb()
-		} else {
-			logger.Errorf("no import db set")
-			os.Exit(1)
-		}
+func indexerDbFromFlags(opts idb.IndexerDbOptions) (db idb.IndexerDb) {
+	if postgresAddr != "" {
+		var err error
+		db, err = idb.IndexerDbByName("postgres", postgresAddr, opts, logger)
+		maybeFail(err, "could not init db, %v", err)
+	} else if dummyIndexerDb {
+		db = dummy.IndexerDb()
+	} else {
+		logger.Errorf("no import db set")
+		os.Exit(1)
 	}
 	return db
 }
@@ -109,7 +107,10 @@ func init() {
 	logger.SetLevel(log.InfoLevel)
 
 	rootCmd.AddCommand(importCmd)
+	importCmd.Hidden = true
 	rootCmd.AddCommand(daemonCmd)
+	rootCmd.AddCommand(resetCmd)
+	resetCmd.Hidden = true
 
 	rootCmd.PersistentFlags().StringVarP(&logLevel, "loglevel", "l", "info", "verbosity of logs: [error, warn, info, debug, trace]")
 	rootCmd.PersistentFlags().StringVarP(&logFile, "logfile", "f", "", "file to write logs to, if unset logs are written to standard out")
diff --git a/cmd/algorand-indexer/reset.go b/cmd/algorand-indexer/reset.go
new file mode 100644
index 0000000..f4dc574
--- /dev/null
+++ b/cmd/algorand-indexer/reset.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/algorand/indexer/config"
+	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/importer"
+)
+
+var (
+	andRebuild bool
+)
+
+var resetCmd = &cobra.Command{
+	Use:   "reset",
+	Short: "run indexer reset",
+	Long:  "run indexer reset. This will delete a lot of data. With --and-rebuild it will be rebuilt now, otherwise algorand-indexer daemon will try to rebuild when next it runs. This is mostly a tool for developers and people who are really really sure they want to do this.",
+	//Args:
+	Run: func(cmd *cobra.Command, args []string) {
+		config.BindFlags(cmd)
+		err := configureLogger()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to configure logger: %v", err)
+			os.Exit(1)
+		}
+		opts := idb.IndexerDbOptions{NoMigrate: true}
+		db := indexerDbFromFlags(opts)
+		timeout := time.Now().Add(5 * time.Second)
+		health, err := db.Health()
+		maybeFail(err, "could not get db health, %v", err)
+		for health.IsMigrating || !health.DBAvailable {
+			if time.Now().After(timeout) {
+				fmt.Fprintf(os.Stderr, "timed out waiting for db to be ready\n")
+				os.Exit(1)
+				return
+			}
+			time.Sleep(100 * time.Millisecond)
+			health, err = db.Health()
+			maybeFail(err, "could not get db health, %v", err)
+		}
+		fmt.Println("Prior to resetting the database make sure no daemons are connected.")
+		fmt.Println("After this command finishes start the daemon again and it will begin to")
+		fmt.Println("re-index the data. This can take hours or days depending on the network")
+		fmt.Println("size. During this time data will be incomplete, similar to the initial import.")
+
+		fmt.Print("\nAre you sure you would like to reset? [y/N] ")
+		scanner := bufio.NewScanner(os.Stdin)
+		if scanner.Scan() {
+			answer := scanner.Text()
+			if len(answer) > 0 && (answer[0] == 'y' || answer[0] == 'Y') {
+				fmt.Println("Resetting...")
+				time.Sleep(2) // leave some ^C time
+				err = db.Reset()
+				maybeFail(err, "database reset failed")
+				if andRebuild {
+					blockround, err := db.GetMaxRoundLoaded()
+					maybeFail(err, "could not find max block loaded, %v", err)
+					if blockround > 0 {
+						fmt.Printf("Done resetting. Re-building accounting through block %d...", blockround)
+						opts.NoMigrate = false
+						// db.Close() // TODO: add Close() to IndxerDb interface?
+						db = indexerDbFromFlags(opts)
+						cache, err := db.GetDefaultFrozen()
+						maybeFail(err, "failed to get default frozen cache")
+						filter := idb.UpdateFilter{
+							MaxRound: blockround,
+						}
+						importer.UpdateAccounting(db, cache, filter, logger)
+						fmt.Println("Done rebuilding accounting.")
+					} else {
+						fmt.Println("Done. No blocks to rebuild accounting from.")
+					}
+				} else {
+					fmt.Println("Done. To re-build, re-start algorand-indexer daemon")
+				}
+				return
+			}
+		}
+		err = scanner.Err()
+		maybeFail(err, "getting prompt char")
+		fmt.Println("not resetting")
+	},
+}
+
+func init() {
+	resetCmd.Flags().BoolVarP(&andRebuild, "and-rebuild", "", false, "re-run accounting after reset (this could take a while)")
+	resetCmd.Flags().StringVarP(&genesisJSONPath, "genesis", "g", "", "path to genesis.json (defaults to genesis.json in algod data dir if that was set)")
+}
diff --git a/cmd/block-generator/generator/generate.go b/cmd/block-generator/generator/generate.go
new file mode 100644
index 0000000..430d223
--- /dev/null
+++ b/cmd/block-generator/generator/generate.go
@@ -0,0 +1,324 @@
+package generator
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"math/rand"
+	"os"
+
+	"github.com/algorand/go-algorand-sdk/encoding/json"
+	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
+
+	"github.com/algorand/indexer/types"
+)
+
+const (
+	consensusTimeMilli int64 = 4500
+)
+
+// GenerationConfig defines the tunable parameters for block generation.
+type GenerationConfig struct {
+	Name                         string `mapstructure:"name"`
+	NumGenesisAccounts           uint64 `mapstructure:"genesis_accounts"`
+	GenesisAccountInitialBalance uint64 `mapstructure:"genesis_account_balance"`
+
+	// Block generation
+	TxnPerBlock uint64 `mapstructure:"tx_per_block"`
+
+	// TX Distribution
+	PaymentTransactionFraction float32 `mapstructure:"tx_pay_fraction"`
+	AssetTransactionFraction   float32 `mapstructure:"tx_asset_fraction"`
+
+	// Payment configuration
+	PaymentNewAccountFraction float32 `mapstructure:"pay_acct_create_fraction"`
+	PaymentTransferFraction   float32 `mapstructure:"pay_xfer_fraction"`
+
+	// Asset configuration
+	AssetCreateFraction  float32 `mapstructure:"asset_create_fraction"`
+	AssetDestroyFraction float32 `mapstructure:"asset_destroy_fraction"`
+	AssetOptinFraction   float32 `mapstructure:"asset_optin_fraction"`
+	AssetCloseFraction   float32 `mapstructure:"asset_close_fraction"`
+	AssetXferFraction    float32 `mapstructure:"asset_xfer_fraction"`
+}
+
+func sumIsCloseToOne(numbers ...float32) bool {
+	var sum float32
+	for _, num := range numbers {
+		sum += num
+	}
+	return sum > 0.99 && sum < 1.01
+}
+
+// MakeGenerator initializes the Generator object.
+func MakeGenerator(config GenerationConfig) (Generator, error) {
+	if !sumIsCloseToOne(config.PaymentTransactionFraction, config.AssetTransactionFraction) {
+		return nil, fmt.Errorf("transaction distribution ratios should equal 1")
+	}
+
+	if !sumIsCloseToOne(config.PaymentNewAccountFraction, config.PaymentTransferFraction) {
+		return nil, fmt.Errorf("payment configuration ratios should equal 1")
+	}
+
+	if !sumIsCloseToOne(config.AssetCreateFraction, config.AssetOptinFraction, config.AssetCloseFraction, config.AssetXferFraction) {
+		return nil, fmt.Errorf("asset configuration ratios should equal 1")
+	}
+
+	gen := &generator{
+		config:                    config,
+		protocol:                  "future",
+		genesisHash:               [32]byte{},
+		genesisID:                 "blockgen-test",
+		prevBlockHash:             "",
+		round:                     0,
+		txnCounter:                0,
+		timestamp:                 0,
+		rewardsLevel:              0,
+		rewardsResidue:            0,
+		rewardsRate:               0,
+		rewardsRecalculationRound: 0,
+	}
+
+	gen.feeSink[31] = 1
+	gen.rewardsPool[31] = 2
+
+	gen.initializeAccounting()
+
+	return gen, nil
+}
+
+// Generator is the interface needed to generate blocks.
+type Generator interface {
+	WriteBlock(output io.Writer, round uint64)
+	WriteGenesis(output io.Writer)
+}
+
+type generator struct {
+	config GenerationConfig
+
+	// payment transaction metadata
+	numPayments uint64
+
+	// Number of algorand accounts
+	numAccounts uint64
+
+	// Block stuff
+	round         uint64
+	txnCounter    uint64
+	prevBlockHash string
+	timestamp     int64
+	protocol      types.ConsensusVersion
+	genesisID     string
+	genesisHash   sdk_types.Digest
+
+	// Rewards stuff
+	feeSink                   types.Address
+	rewardsPool               types.Address
+	rewardsLevel              uint64
+	rewardsResidue            uint64
+	rewardsRate               uint64
+	rewardsRecalculationRound uint64
+
+	// balances To avoid crypto and reduce storage, accounts are faked.
+	// The account is based on the index into the balances array.
+	balances []uint64
+
+	assetBalances map[uint32][]uint64
+	numAssets     uint64
+}
+
+func (g *generator) WriteGenesis(output io.Writer) {
+	var allocations []types.GenesisAllocation
+
+	for i := uint64(0); i < g.config.NumGenesisAccounts; i++ {
+		addr := indexToAccount(i)
+		allocations = append(allocations, types.GenesisAllocation{
+			Address: addr.String(),
+			State: types.AccountData{
+				MicroAlgos: types.MicroAlgos(g.config.GenesisAccountInitialBalance),
+			},
+		})
+	}
+
+	gen := types.Genesis{
+		SchemaID:    "v1",
+		Network:     "generated-network",
+		Proto:       g.protocol,
+		Allocation:  allocations,
+		RewardsPool: g.rewardsPool.String(),
+		FeeSink:     g.feeSink.String(),
+		Timestamp:   g.timestamp,
+	}
+
+	output.Write(json.Encode(gen))
+}
+
+func (g *generator) generateTransaction(round uint64, intra uint64) (types.SignedTxnWithAD, error) {
+	// TODO: Distribute transactions according to configuration
+	return g.generatePaymentTxn(round, intra)
+}
+
+// WriteBlock generates a block full of new transactions and writes it to the writer.
+func (g *generator) WriteBlock(output io.Writer, round uint64) {
+	if round != g.round {
+		fmt.Printf("Generator only supports sequential block access. Expected %d but received request for %d.", g.round, round)
+	}
+
+	// Generate the transactions
+	transactions := make([]types.SignedTxnInBlock, 0, g.config.TxnPerBlock)
+	for i := uint64(0); i < g.config.TxnPerBlock; i++ {
+		txn, err := g.generateTransaction(g.round, i)
+		stxnib := types.SignedTxnInBlock{
+			SignedTxnWithAD: txn,
+			HasGenesisID:    true,
+			HasGenesisHash:  true,
+		}
+		if err != nil {
+			panic(fmt.Sprintf("failed to generate transaction: %v\n", err))
+		}
+		transactions = append(transactions, stxnib)
+	}
+
+	block := types.Block{
+		BlockHeader: types.BlockHeader{
+			Round:       types.Round(g.round),
+			Branch:      types.BlockHash{},
+			Seed:        types.Seed{},
+			TxnRoot:     types.Digest{},
+			TimeStamp:   g.timestamp,
+			GenesisID:   g.genesisID,
+			GenesisHash: g.genesisHash,
+			RewardsState: types.RewardsState{
+				FeeSink:                   g.feeSink,
+				RewardsPool:               g.rewardsPool,
+				RewardsLevel:              0,
+				RewardsRate:               0,
+				RewardsResidue:            0,
+				RewardsRecalculationRound: 0,
+			},
+			UpgradeState: types.UpgradeState{
+				CurrentProtocol: g.protocol,
+			},
+			UpgradeVote: types.UpgradeVote{},
+			TxnCounter:  g.txnCounter,
+			CompactCert: nil,
+		},
+		Payset: types.Payset(transactions),
+	}
+
+	cert := types.EncodedBlockCert{
+		Block:       block,
+		Certificate: types.Certificate{},
+	}
+
+	g.txnCounter += g.config.TxnPerBlock
+	g.timestamp += consensusTimeMilli
+	g.round++
+
+	fmt.Println(g.txnCounter)
+	output.Write(msgpack.Encode(cert))
+}
+
+func indexToAccount(i uint64) (addr sdk_types.Address) {
+	binary.LittleEndian.PutUint64(addr[:], i)
+	return
+}
+
+// initializeAccounting creates the genesis accounts.
+func (g *generator) initializeAccounting() {
+	if g.config.NumGenesisAccounts == 0 {
+		panic("Number of genesis accounts must be > 0.")
+	}
+
+	g.numPayments = 0
+	g.numAccounts = g.config.NumGenesisAccounts
+	for i := uint64(0); i < g.config.NumGenesisAccounts; i++ {
+		g.balances = append(g.balances, g.config.GenesisAccountInitialBalance)
+	}
+}
+
+// convert wraps the transaction as needed for including in the block.
+func convert(txn sdk_types.Transaction, ad types.ApplyData) types.SignedTxnWithAD {
+	stxn := sdk_types.SignedTxn{
+		Sig:      sdk_types.Signature{},
+		Msig:     sdk_types.MultisigSig{},
+		Lsig:     sdk_types.LogicSig{},
+		Txn:      txn,
+		AuthAddr: sdk_types.Address{},
+	}
+
+	// TODO: Would it be useful to generate a random signature?
+	stxn.Sig[32] = 50
+
+	withAd := types.SignedTxnWithAD{
+		SignedTxn: stxn,
+		// TODO: Add close-amount to apply data
+		ApplyData: ad,
+	}
+
+	return withAd
+}
+
+// generatePaymentTxn creates a new payment transaction. The sender is always a genesis account, the receiver is random,
+// or a new account.
+func (g *generator) generatePaymentTxn(round uint64, intra uint64) (types.SignedTxnWithAD, error) {
+	var receiveIndex uint64
+	if g.numPayments%uint64(100*g.config.PaymentNewAccountFraction) == 0 {
+		g.balances = append(g.balances, 0)
+		g.numAccounts++
+		receiveIndex = g.numAccounts - 1
+	} else {
+		receiveIndex = rand.Uint64() % g.numAccounts
+	}
+
+	// Always send from a genesis account.
+	sendIndex := g.numPayments % g.config.NumGenesisAccounts
+
+	sender := indexToAccount(sendIndex)
+	receiver := indexToAccount(receiveIndex)
+
+	amount := uint64(10000000)
+	fee := uint64(1000)
+	if g.balances[sendIndex] < 2 {
+		fmt.Printf(fmt.Sprintf("\n\nthe sender account does not enough algos for the transfer. idx %d, payment number %d\n\n", sendIndex, g.numPayments))
+		os.Exit(1)
+	}
+
+	g.balances[sendIndex] -= amount
+	g.balances[sendIndex] -= fee
+	g.balances[receiveIndex] += amount
+
+	g.numPayments++
+	txn := sdk_types.Transaction{
+		Type: "pay",
+		Header: sdk_types.Header{
+			Sender:      sender,
+			Fee:         sdk_types.MicroAlgos(fee),
+			FirstValid:  sdk_types.Round(round),
+			LastValid:   sdk_types.Round(round + 1000),
+			Note:        nil,
+			GenesisID:   g.genesisID,
+			GenesisHash: g.genesisHash,
+			Group:       sdk_types.Digest{},
+			Lease:       [32]byte{},
+			RekeyTo:     sdk_types.Address{},
+		},
+		KeyregTxnFields: sdk_types.KeyregTxnFields{},
+		PaymentTxnFields: sdk_types.PaymentTxnFields{
+			Receiver:         receiver,
+			Amount:           sdk_types.MicroAlgos(amount),
+			CloseRemainderTo: sdk_types.Address{},
+		},
+		AssetConfigTxnFields:   sdk_types.AssetConfigTxnFields{},
+		AssetTransferTxnFields: sdk_types.AssetTransferTxnFields{},
+		AssetFreezeTxnFields:   sdk_types.AssetFreezeTxnFields{},
+		ApplicationFields:      sdk_types.ApplicationFields{},
+	}
+
+	return convert(txn, types.ApplyData{}), nil
+}
+
+func (g *generator) generateAssetTxn(round uint64, intra uint64) (types.SignedTxnWithAD, error) {
+	return convert(sdk_types.Transaction{}, types.ApplyData{}), nil
+}
diff --git a/cmd/block-generator/main.go b/cmd/block-generator/main.go
new file mode 100644
index 0000000..bcfceea
--- /dev/null
+++ b/cmd/block-generator/main.go
@@ -0,0 +1,120 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"math/rand"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/spf13/viper"
+
+	"github.com/algorand/indexer/cmd/block-generator/generator"
+	"github.com/algorand/indexer/util"
+)
+
+var configFile string
+var port uint
+var gen generator.Generator
+
+const configFileName = "block_generator_config"
+
+func init() {
+	rand.Seed(12345)
+	flag.StringVar(&configFile, "config", "", fmt.Sprintf("Override default config file from '%s'.", configFileName))
+	flag.UintVar(&port, "port", 4010, "Port to start the server at.")
+
+	viper.SetConfigName(configFileName)
+	viper.SetConfigType("yaml")
+	viper.AddConfigPath(".")
+}
+
+func initializeConfigFile() (config generator.GenerationConfig, err error) {
+	if len(configFile) > 0 {
+		var f *os.File
+		f, err = os.Open(configFile)
+		if err != nil {
+			return
+		}
+
+		err = viper.ReadConfig(f)
+	} else {
+		err = viper.ReadInConfig()
+	}
+
+	// Problem reading config
+	if err != nil {
+		return
+	}
+
+	err = viper.Unmarshal(&config)
+	return
+}
+
+func main() {
+	flag.Parse()
+
+	config, err := initializeConfigFile()
+	util.MaybeFail(err, "problem loading config file. Use '-config' or create a config file.")
+
+	gen, err = generator.MakeGenerator(config)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Unable to create generator: %v", err)
+		os.Exit(1)
+	}
+
+	http.HandleFunc("/", help)
+	http.HandleFunc("/v2/blocks/", handleBlock)
+	http.HandleFunc("/genesis", handleGenesis)
+
+	portStr := fmt.Sprintf(":%d", port)
+	fmt.Printf("Starting server at %s\n", portStr)
+	http.ListenAndServe(portStr, nil)
+}
+
+func help(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "Use /v2/blocks/:blocknum: to get a block.")
+}
+
+func handleGenesis(w http.ResponseWriter, r *http.Request) {
+	gen.WriteGenesis(w)
+}
+
+func handleBlock(w http.ResponseWriter, r *http.Request) {
+	// The generator doesn't actually care about the block...
+	round, err := parseRound(r.URL.Path)
+	if err != nil {
+		fmt.Fprintf(w, err.Error())
+		return
+	}
+
+	gen.WriteBlock(w, round)
+}
+
+const blockQueryPrefix = "/v2/blocks/"
+const blockQueryBlockIdx = len(blockQueryPrefix)
+
+func parseRound(path string) (uint64, error) {
+	if !strings.HasPrefix(path, blockQueryPrefix) {
+		return 0, fmt.Errorf("not a blocks query: %s", path)
+	}
+
+	result := uint64(0)
+	pathlen := len(path)
+
+	if pathlen == blockQueryBlockIdx {
+		return 0, fmt.Errorf("no block in path")
+	}
+
+	for i := blockQueryBlockIdx; i < pathlen; i++ {
+		if path[i] < '0' || path[i] > '9' {
+			if i == blockQueryBlockIdx {
+				return 0, fmt.Errorf("no block in path")
+			}
+			break
+		}
+		result = (uint64(10) * result) + uint64(int(path[i])-'0')
+	}
+	return result, nil
+}
diff --git a/cmd/block-generator/run_generator.sh b/cmd/block-generator/run_generator.sh
new file mode 100755
index 0000000..6eccee2
--- /dev/null
+++ b/cmd/block-generator/run_generator.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+# Demonstrate how to run the generator and connect it to indexer.
+
+POSTGRES_CONTAINER=generator-test-container
+POSTGRES_PORT=15432
+POSTGRES_DATABASE=generator_db
+
+function start_postgres() {
+  docker rm -f $POSTGRES_CONTAINER > /dev/null 2>&1 || true
+
+  # Start postgres container...
+  docker run \
+     -d \
+     --name $POSTGRES_CONTAINER \
+     -e POSTGRES_USER=algorand \
+     -e POSTGRES_PASSWORD=algorand \
+     -e PGPASSWORD=algorand \
+     -p $POSTGRES_PORT:5432 \
+     postgres
+ 
+   sleep 5
+
+  docker exec -it $POSTGRES_CONTAINER psql -Ualgorand -c "create database $POSTGRES_DATABASE"
+}
+
+function shutdown() {
+  docker rm -f $POSTGRES_CONTAINER > /dev/null 2>&1 || true
+  kill -9 $GENERATOR_PID
+}
+
+trap shutdown EXIT
+
+echo "Building generator."
+go build
+pushd ../.. > /dev/null
+echo "Building indexer."
+make
+echo "Starting postgres container."
+start_postgres
+echo "Starting block generator (see generator.log)"
+./cmd/block-generator/block-generator -port 11111 -config cmd/block-generator/test_config.yml &
+GENERATOR_PID=$!
+echo "Starting indexer"
+./cmd/algorand-indexer/algorand-indexer daemon \
+              -S localhost:8980 \
+              --algod-net localhost:11111 \
+              --algod-token security-is-our-number-one-priority \
+              -P "host=localhost user=algorand password=algorand dbname=generator_db port=15432 sslmode=disable"
diff --git a/cmd/block-generator/scenarios/config.asset.close.yml b/cmd/block-generator/scenarios/config.asset.close.yml
new file mode 100644
index 0000000..6dfeb3d
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.asset.close.yml
@@ -0,0 +1,13 @@
+name: "Asset Close"
+genesis_acocunts: 1000000
+genesis_account_balance: 100000000
+tx_per_block: 5000
+
+# transaction distribution
+tx_asset_fraction: 1.0
+
+# asset config
+asset_create_fraction: 0.001
+asset_optin_fraction: 0.50
+asset_close_fraction: 0.40
+asset_xfer_fraction: 0.099
diff --git a/cmd/block-generator/scenarios/config.asset.destroy.yml b/cmd/block-generator/scenarios/config.asset.destroy.yml
new file mode 100644
index 0000000..ca8fed5
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.asset.destroy.yml
@@ -0,0 +1,13 @@
+name: "Asset Destroy"
+genesis_acocunts: 1000000
+genesis_account_balance: 100000000
+tx_per_block: 5000
+
+# transaction distribution
+tx_asset_fraction: 1.0
+
+# asset config
+asset_create_fraction: 0.8
+asset_destroy_fraction: 0.2
+asset_optin_fraction: 0.0
+asset_close_fraction: 0.0
diff --git a/cmd/block-generator/scenarios/config.asset.xfer.yml b/cmd/block-generator/scenarios/config.asset.xfer.yml
new file mode 100644
index 0000000..5e2d759
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.asset.xfer.yml
@@ -0,0 +1,12 @@
+name: "Asset Xfer"
+genesis_acocunts: 1000000
+genesis_account_balance: 100000000
+tx_per_block: 5000
+
+# transaction distribution
+tx_asset_fraction: 1.0
+
+# asset config
+asset_create_fraction: 0.001
+asset_optin_fraction: 0.10
+asset_xfer_fraction: 0.899
diff --git a/cmd/block-generator/scenarios/config.mixed.jumbo.yml b/cmd/block-generator/scenarios/config.mixed.jumbo.yml
new file mode 100644
index 0000000..31c25a3
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.mixed.jumbo.yml
@@ -0,0 +1,19 @@
+name: "Mixed (jumbo)"
+genesis_acocunts: 1000000
+genesis_account_balance: 100000000
+tx_per_block: 50000
+
+# transaction distribution
+tx_pay_fraction: 0.3
+tx_asset_fraction: 0.7
+
+# payment config
+pay_acct_create_fraction: 0.02
+pay_xfer_fraction: 0.98
+
+# asset config
+asset_create_fraction: 0.001
+asset_optin_fraction: 0.1
+asset_close_fraction: 0.05
+asset_xfer_fraction: 0.849
+asset_delete_fraction: 0
diff --git a/cmd/block-generator/scenarios/config.mixed.yml b/cmd/block-generator/scenarios/config.mixed.yml
new file mode 100644
index 0000000..bb892da
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.mixed.yml
@@ -0,0 +1,19 @@
+name: "Mixed"
+genesis_acocunts: 1000000
+genesis_account_balance: 100000000
+tx_per_block: 5000
+
+# transaction distribution
+tx_pay_fraction: 0.3
+tx_asset_fraction: 0.7
+
+# payment config
+pay_acct_create_fraction: 0.02
+pay_xfer_fraction: 0.98
+
+# asset config
+asset_create_fraction: 0.001
+asset_optin_fraction: 0.1
+asset_close_fraction: 0.05
+asset_xfer_fraction: 0.849
+asset_delete_fraction: 0
diff --git a/cmd/block-generator/scenarios/config.payment.full.yml b/cmd/block-generator/scenarios/config.payment.full.yml
new file mode 100644
index 0000000..22c96fc
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.payment.full.yml
@@ -0,0 +1,11 @@
+name: "Pay (full)"
+genesis_acocunts: 10
+genesis_account_balance: 1000000000
+tx_per_block: 5000
+
+# transaction distribution
+tx_pay_fraction: 1.0
+
+# payment config
+pay_acct_create_fraction: 0.02
+pay_xfer_fraction: 0.98
diff --git a/cmd/block-generator/scenarios/config.payment.jumbo.yml b/cmd/block-generator/scenarios/config.payment.jumbo.yml
new file mode 100644
index 0000000..2294b46
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.payment.jumbo.yml
@@ -0,0 +1,11 @@
+name: "Pay (jumbo)"
+genesis_acocunts: 10
+genesis_account_balance: 1000000000
+tx_per_block: 50000
+
+# transaction distribution
+tx_pay_fraction: 1.0
+
+# payment config
+pay_acct_create_fraction: 0.02
+pay_xfer_fraction: 0.98
diff --git a/cmd/block-generator/scenarios/config.payment.small.yml b/cmd/block-generator/scenarios/config.payment.small.yml
new file mode 100644
index 0000000..4dcbb64
--- /dev/null
+++ b/cmd/block-generator/scenarios/config.payment.small.yml
@@ -0,0 +1,11 @@
+name: "Pay (small)"
+genesis_acocunts: 10
+genesis_account_balance: 1000000000
+tx_per_block: 100
+
+# transaction distribution
+tx_pay_fraction: 1.0
+
+# payment config
+pay_acct_create_fraction: 0.02
+pay_xfer_fraction: 0.98
diff --git a/cmd/texttosource/main.go b/cmd/texttosource/main.go
index 0d0be5f..f3afcd7 100644
--- a/cmd/texttosource/main.go
+++ b/cmd/texttosource/main.go
@@ -18,14 +18,12 @@ func main() {
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "%s: %v\n", fname, err)
 			os.Exit(1)
-			return
 		}
 		outname := strings.ReplaceAll(fname, ".", "_") + ".go"
 		fout, err := os.Create(outname)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "%s: %v\n", outname, err)
 			os.Exit(1)
-			return
 		}
 		varname := strings.ReplaceAll(fname, ".", "_")
 		bodyConstant := "`" + strings.ReplaceAll(string(data), "`", "\\u0060") + "`"
@@ -35,5 +33,9 @@ package %s
 
 const %s = %s
 `, fname, packageName, varname, bodyConstant)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to write to %s: %v\n", outname, err)
+			os.Exit(1)
+		}
 	}
 }
diff --git a/cmd/validator/validate_accounting.go b/cmd/validator/validate_accounting.go
index 8d5a1bd..5cec445 100644
--- a/cmd/validator/validate_accounting.go
+++ b/cmd/validator/validate_accounting.go
@@ -15,7 +15,7 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 )
 
 // Params are the program arguments which need to be passed between objects.
@@ -152,14 +152,14 @@ func start(processor Processor, threads int, config Params, results chan<- Resul
 
 // normalizeAddress accepts an algorand address or base64 encoded address and outputs the algorand address
 func normalizeAddress(addr string) (string, error) {
-	_, err := types.DecodeAddress(addr)
+	_, err := sdk_types.DecodeAddress(addr)
 	if err == nil {
 		return addr, nil
 	}
 
 	addrBytes, err := base64.StdEncoding.DecodeString(addr)
 	if err == nil {
-		var address types.Address
+		var address sdk_types.Address
 		copy(address[:], addrBytes)
 		return address.String(), nil
 	}
diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go
index 0df872d..455b027 100644
--- a/fetcher/fetcher.go
+++ b/fetcher/fetcher.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/algorand/go-algorand-sdk/client/v2/algod"
@@ -53,10 +54,14 @@ type fetcherImpl struct {
 
 	log *log.Logger
 
-	err error
+	err   error // protected by `errmu`
+	errmu sync.Mutex
 }
 
 func (bot *fetcherImpl) Error() string {
+	bot.errmu.Lock()
+	defer bot.errmu.Unlock()
+
 	if bot.err != nil {
 		return bot.err.Error()
 	}
@@ -84,6 +89,12 @@ func (bot *fetcherImpl) isDone() bool {
 	}
 }
 
+func (bot *fetcherImpl) setError(err error) {
+	bot.errmu.Lock()
+	bot.err = err
+	bot.errmu.Unlock()
+}
+
 // fetch the next block by round number until we find one missing (because it doesn't exist yet)
 func (bot *fetcherImpl) catchupLoop() {
 	var err error
@@ -96,14 +107,14 @@ func (bot *fetcherImpl) catchupLoop() {
 
 		blockbytes, err = aclient.BlockRaw(bot.nextRound).Do(context.Background())
 		if err != nil {
-			bot.err = err
+			bot.setError(err)
 			bot.log.WithError(err).Errorf("catchup block %d", bot.nextRound)
 			return
 		}
 
 		err = bot.handleBlockBytes(blockbytes)
 		if err != nil {
-			bot.err = err
+			bot.setError(err)
 			bot.log.WithError(err).Errorf("err handling catchup block %d", bot.nextRound)
 			return
 		}
@@ -134,17 +145,17 @@ func (bot *fetcherImpl) followLoop() {
 			bot.log.WithError(err).Errorf("r=%d err getting block %d", retries, bot.nextRound)
 		}
 		if err != nil {
-			bot.err = err
+			bot.setError(err)
 			return
 		}
 		err = bot.handleBlockBytes(blockbytes)
 		if err != nil {
-			bot.err = err
+			bot.setError(err)
 			bot.log.WithError(err).Errorf("err handling follow block %d", bot.nextRound)
 			break
 		}
 		// If we successfully handle the block, clear out any transient error which may have occurred.
-		bot.err = nil
+		bot.setError(nil)
 		bot.nextRound++
 		bot.failingSince = time.Time{}
 	}
@@ -172,7 +183,7 @@ func (bot *fetcherImpl) Run() {
 		time.Sleep(5 * time.Second)
 		err := bot.reclient()
 		if err != nil {
-			bot.err = err
+			bot.setError(err)
 			bot.log.WithError(err).Errorln("err trying to re-client")
 		} else {
 			bot.log.Infof("reclient happened")
@@ -190,9 +201,9 @@ func (bot *fetcherImpl) SetNextRound(nextRound uint64) {
 	bot.nextRound = nextRound
 }
 
-func (bot *fetcherImpl) handleBlockBytes(blockbytes []byte) (err error) {
+func (bot *fetcherImpl) handleBlockBytes(blockbytes []byte) error {
 	var block types.EncodedBlockCert
-	err = msgpack.Decode(blockbytes, &block)
+	err := msgpack.Decode(blockbytes, &block)
 	if err != nil {
 		if bot.log.IsLevelEnabled(log.DebugLevel) {
 			var generic map[string]interface{}
@@ -202,13 +213,18 @@ func (bot *fetcherImpl) handleBlockBytes(blockbytes []byte) (err error) {
 			}
 		}
 
-		err = fmt.Errorf("unable to decode block: %v", err)
-		return
+		return fmt.Errorf("unable to decode block: %v", err)
+	}
+
+	if block.Block.Round != types.Round(bot.nextRound) {
+		return fmt.Errorf("expected round %d but got %d", bot.nextRound, block.Block.Round)
 	}
+
 	for _, handler := range bot.blockHandlers {
 		handler.HandleBlock(&block)
 	}
-	return
+
+	return nil
 }
 
 // AddBlockHandler is part of the Fetcher interface
@@ -267,11 +283,6 @@ func (bot *fetcherImpl) reclient() (err error) {
 	return
 }
 
-func algodUpdated(datadir string) (lastmod time.Time, err error) {
-	netpath, tokenpath := algodPaths(datadir)
-	return algodStat(netpath, tokenpath)
-}
-
 func algodPaths(datadir string) (netpath, tokenpath string) {
 	netpath = filepath.Join(datadir, "algod.net")
 	tokenpath = filepath.Join(datadir, "algod.token")
diff --git a/go.mod b/go.mod
index 121c0c5..638b9ad 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/algorand/indexer
 go 1.13
 
 require (
-	github.com/algorand/go-algorand-sdk v1.5.1
+	github.com/algorand/go-algorand-sdk v1.7.0
 	github.com/algorand/go-codec/codec v1.1.7
 	github.com/algorand/oapi-codegen v1.3.5-algorand5
 	github.com/getkin/kin-openapi v0.19.0
diff --git a/go.sum b/go.sum
index 5b2f703..c8bbb0b 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/algorand/go-algorand-sdk v1.5.1 h1:5THZvMOlfAc+HHRhbV/V89LJC4k0EzhK1mn3oizSt+c=
-github.com/algorand/go-algorand-sdk v1.5.1/go.mod h1:U12d8fTN/CyKPR1HObrt51ITxb6OgXxpGCH743Ds2GQ=
+github.com/algorand/go-algorand-sdk v1.7.0 h1:eRg9UQy7FndRE5nTgniDpbyjTvP2FUKX4/0e8NdXn/E=
+github.com/algorand/go-algorand-sdk v1.7.0/go.mod h1:U12d8fTN/CyKPR1HObrt51ITxb6OgXxpGCH743Ds2GQ=
 github.com/algorand/go-codec v1.1.7 h1:6nvCh2nfgnfkaoVHKQyk2wxyl2GQBAlI7IkbqbB/e4s=
 github.com/algorand/go-codec v1.1.7/go.mod h1:pVLQYhIVCsx9D3iy4W4Qqi0SKhx6IVhMwOvj/agFL4g=
 github.com/algorand/go-codec/codec v1.1.7 h1:EFOyWf5duxbh2ru+AW1YDgmZ+MRVgqklELSqTArgp3M=
@@ -304,7 +304,6 @@ github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz
 github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
 github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
-github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.5.1 h1:Jn6HYxiYrtQ92CopqJLvfPCJUrrruw1+1cn0jM9dKrI=
 github.com/lib/pq v1.5.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
diff --git a/idb/dummy/dummy.go b/idb/dummy/dummy.go
new file mode 100644
index 0000000..5d8422b
--- /dev/null
+++ b/idb/dummy/dummy.go
@@ -0,0 +1,122 @@
+package dummy
+
+import (
+	"context"
+
+	log "github.com/sirupsen/logrus"
+
+	models "github.com/algorand/indexer/api/generated/v2"
+	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/types"
+)
+
+type dummyIndexerDb struct {
+	log *log.Logger
+}
+
+// IndexerDb is a mock implementation of IndexerDb
+func IndexerDb() idb.IndexerDb {
+	return &dummyIndexerDb{}
+}
+
+// StartBlock is part of idb.IndexerDB
+func (db *dummyIndexerDb) StartBlock() (err error) {
+	db.log.Printf("StartBlock")
+	return nil
+}
+
+// AddTransaction is part of idb.IndexerDB
+func (db *dummyIndexerDb) AddTransaction(round uint64, intra int, txtypeenum int, assetid uint64, txn types.SignedTxnWithAD, participation [][]byte) error {
+	db.log.Printf("\ttxn %d %d %d %d", round, intra, txtypeenum, assetid)
+	return nil
+}
+
+// CommitBlock is part of idb.IndexerDB
+func (db *dummyIndexerDb) CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error {
+	db.log.Printf("CommitBlock %d %d %d header bytes", round, timestamp, len(headerbytes))
+	return nil
+}
+
+// LoadGenesis is part of idb.IndexerDB
+func (db *dummyIndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
+	return nil
+}
+
+// GetImportState is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetImportState() (is idb.ImportState, err error) {
+	return idb.ImportState{}, nil
+}
+
+// SetImportState is part of idb.IndexerDB
+func (db *dummyIndexerDb) SetImportState(is idb.ImportState) (err error) {
+	return nil
+}
+
+// GetMaxRoundAccounted is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetMaxRoundAccounted() (round uint64, err error) {
+	return 0, nil
+}
+
+// GetMaxRoundLoaded is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetMaxRoundLoaded() (round uint64, err error) {
+	return 0, nil
+}
+
+// GetSpecialAccounts is part of idb.IndexerDb
+func (db *dummyIndexerDb) GetSpecialAccounts() (idb.SpecialAccounts, error) {
+	return idb.SpecialAccounts{}, nil
+}
+
+// GetDefaultFrozen is part of idb.IndexerDb
+func (db *dummyIndexerDb) GetDefaultFrozen() (defaultFrozen map[uint64]bool, err error) {
+	return make(map[uint64]bool), nil
+}
+
+// YieldTxns is part of idb.IndexerDB
+func (db *dummyIndexerDb) YieldTxns(ctx context.Context, firstRound uint64) <-chan idb.TxnRow {
+	return nil
+}
+
+// CommitRoundAccounting is part of idb.IndexerDB
+func (db *dummyIndexerDb) CommitRoundAccounting(updates idb.RoundUpdates, round uint64, blockPtr *types.Block) (err error) {
+	return nil
+}
+
+// GetBlock is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetBlock(ctx context.Context, round uint64, options idb.GetBlockOptions) (block types.Block, transactions []idb.TxnRow, err error) {
+	return types.Block{}, nil, nil
+}
+
+// Transactions is part of idb.IndexerDB
+func (db *dummyIndexerDb) Transactions(ctx context.Context, tf idb.TransactionFilter) (<-chan idb.TxnRow, uint64) {
+	return nil, 0
+}
+
+// GetAccounts is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetAccounts(ctx context.Context, opts idb.AccountQueryOptions) (<-chan idb.AccountRow, uint64) {
+	return nil, 0
+}
+
+// Assets is part of idb.IndexerDB
+func (db *dummyIndexerDb) Assets(ctx context.Context, filter idb.AssetsQuery) (<-chan idb.AssetRow, uint64) {
+	return nil, 0
+}
+
+// AssetBalances is part of idb.IndexerDB
+func (db *dummyIndexerDb) AssetBalances(ctx context.Context, abq idb.AssetBalanceQuery) (<-chan idb.AssetBalanceRow, uint64) {
+	return nil, 0
+}
+
+// Applications is part of idb.IndexerDB
+func (db *dummyIndexerDb) Applications(ctx context.Context, filter *models.SearchForApplicationsParams) (<-chan idb.ApplicationRow, uint64) {
+	return nil, 0
+}
+
+// Health is part of idb.IndexerDB
+func (db *dummyIndexerDb) Health() (state idb.Health, err error) {
+	return idb.Health{}, nil
+}
+
+func (db *dummyIndexerDb) Reset() (err error) {
+	return nil
+}
diff --git a/idb/dummy/dummy_factory.go b/idb/dummy/dummy_factory.go
new file mode 100644
index 0000000..310dae3
--- /dev/null
+++ b/idb/dummy/dummy_factory.go
@@ -0,0 +1,24 @@
+package dummy
+
+import (
+	log "github.com/sirupsen/logrus"
+
+	"github.com/algorand/indexer/idb"
+)
+
+type dummyFactory struct {
+}
+
+// Name is part of the IndexerFactory interface.
+func (df dummyFactory) Name() string {
+	return "dummy"
+}
+
+// Build is part of the IndexerFactory interface.
+func (df dummyFactory) Build(arg string, opts idb.IndexerDbOptions, log *log.Logger) (idb.IndexerDb, error) {
+	return &dummyIndexerDb{log: log}, nil
+}
+
+func init() {
+	idb.RegisterFactory("dummy", &dummyFactory{})
+}
diff --git a/idb/dummy.go b/idb/idb.go
similarity index 68%
rename from idb/dummy.go
rename to idb/idb.go
index e4cad48..f1f63b7 100644
--- a/idb/dummy.go
+++ b/idb/idb.go
@@ -6,152 +6,18 @@ import (
 	"encoding/base32"
 	"encoding/base64"
 	"encoding/binary"
+	"errors"
 	"fmt"
 	"math/big"
 	"strings"
 	"time"
 
-	atypes "github.com/algorand/go-algorand-sdk/types"
-	log "github.com/sirupsen/logrus"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	models "github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/types"
 )
 
-// DummyIndexerDb is a mock implementation of IndexerDb
-func DummyIndexerDb() IndexerDb {
-	return &dummyIndexerDb{}
-}
-
-type dummyIndexerDb struct {
-	log *log.Logger
-}
-
-// StartBlock is part of idb.IndexerDB
-func (db *dummyIndexerDb) StartBlock() (err error) {
-	db.log.Printf("StartBlock")
-	return nil
-}
-
-// AddTransaction is part of idb.IndexerDB
-func (db *dummyIndexerDb) AddTransaction(round uint64, intra int, txtypeenum int, assetid uint64, txn types.SignedTxnWithAD, participation [][]byte) error {
-	db.log.Printf("\ttxn %d %d %d %d", round, intra, txtypeenum, assetid)
-	return nil
-}
-
-// CommitBlock is part of idb.IndexerDB
-func (db *dummyIndexerDb) CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error {
-	db.log.Printf("CommitBlock %d %d %d header bytes", round, timestamp, len(headerbytes))
-	return nil
-}
-
-// AlreadyImported is part of idb.IndexerDB
-func (db *dummyIndexerDb) AlreadyImported(path string) (imported bool, err error) {
-	return false, nil
-}
-
-// MarkImported is part of idb.IndexerDB
-func (db *dummyIndexerDb) MarkImported(path string) (err error) {
-	return nil
-}
-
-// LoadGenesis is part of idb.IndexerDB
-func (db *dummyIndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
-	return nil
-}
-
-// SetProto is part of idb.IndexerDB
-func (db *dummyIndexerDb) SetProto(version string, proto types.ConsensusParams) (err error) {
-	return nil
-}
-
-// GetProto is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetProto(version string) (proto types.ConsensusParams, err error) {
-	err = nil
-	return
-}
-
-// GetImportState is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetImportState() (is *ImportState, err error) {
-	return nil, nil
-}
-
-// SetImportState is part of idb.IndexerDB
-func (db *dummyIndexerDb) SetImportState(is ImportState) (err error) {
-	return nil
-}
-
-// GetMaxRoundAccounted is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetMaxRoundAccounted() (round uint64, err error) {
-	return 0, nil
-}
-
-// GetMaxRoundLoaded is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetMaxRoundLoaded() (round uint64, err error) {
-	return 0, nil
-}
-
-// GetSpecialAccounts is part of idb.IndexerDb
-func (db *dummyIndexerDb) GetSpecialAccounts() (SpecialAccounts, error) {
-	return SpecialAccounts{}, nil
-}
-
-// GetDefaultFrozen is part of idb.IndexerDb
-func (db *dummyIndexerDb) GetDefaultFrozen() (defaultFrozen map[uint64]bool, err error) {
-	return make(map[uint64]bool), nil
-}
-
-// YieldTxns is part of idb.IndexerDB
-func (db *dummyIndexerDb) YieldTxns(ctx context.Context, prevRound int64) <-chan TxnRow {
-	return nil
-}
-
-// CommitRoundAccounting is part of idb.IndexerDB
-func (db *dummyIndexerDb) CommitRoundAccounting(updates RoundUpdates, round uint64, blockPtr *types.Block) (err error) {
-	return nil
-}
-
-// GetBlock is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetBlock(ctx context.Context, round uint64, options GetBlockOptions) (block types.Block, transactions []TxnRow, err error) {
-	return types.Block{}, nil, nil
-}
-
-// Transactions is part of idb.IndexerDB
-func (db *dummyIndexerDb) Transactions(ctx context.Context, tf TransactionFilter) (<-chan TxnRow, uint64) {
-	return nil, 0
-}
-
-// GetAccounts is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetAccounts(ctx context.Context, opts AccountQueryOptions) (<-chan AccountRow, uint64) {
-	return nil, 0
-}
-
-// Assets is part of idb.IndexerDB
-func (db *dummyIndexerDb) Assets(ctx context.Context, filter AssetsQuery) (<-chan AssetRow, uint64) {
-	return nil, 0
-}
-
-// AssetBalances is part of idb.IndexerDB
-func (db *dummyIndexerDb) AssetBalances(ctx context.Context, abq AssetBalanceQuery) (<-chan AssetBalanceRow, uint64) {
-	return nil, 0
-}
-
-// Applications is part of idb.IndexerDB
-func (db *dummyIndexerDb) Applications(ctx context.Context, filter *models.SearchForApplicationsParams) (<-chan ApplicationRow, uint64) {
-	return nil, 0
-}
-
-// Health is part of idb.IndexerDB
-func (db *dummyIndexerDb) Health() (state Health, err error) {
-	return Health{}, nil
-}
-
-// IndexerFactory is used to install an IndexerDb implementation.
-type IndexerFactory interface {
-	Name() string
-	Build(arg string, opts *IndexerDbOptions, log *log.Logger) (IndexerDb, error)
-}
-
 // TxnRow is metadata relating to one transaction in a transaction query.
 type TxnRow struct {
 	// Round is the round where the transaction was committed.
@@ -203,6 +69,10 @@ type TxnExtra struct {
 	LocalReverseDelta  AppReverseDelta `codec:"alr,omitempty"`
 }
 
+// ErrorNotInitialized is used when requesting something that can't be returned
+// because initialization has not been completed.
+var ErrorNotInitialized error = errors.New("accounting not initialized")
+
 // IndexerDb is the interface used to define alternative Indexer backends.
 // TODO: sqlite3 impl
 // TODO: cockroachdb impl
@@ -212,22 +82,20 @@ type IndexerDb interface {
 	AddTransaction(round uint64, intra int, txtypeenum int, assetid uint64, txn types.SignedTxnWithAD, participation [][]byte) error
 	CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error
 
-	AlreadyImported(path string) (imported bool, err error)
-	MarkImported(path string) (err error)
-
 	LoadGenesis(genesis types.Genesis) (err error)
-	SetProto(version string, proto types.ConsensusParams) (err error)
-	GetProto(version string) (proto types.ConsensusParams, err error)
 
-	GetImportState() (is *ImportState, err error)
+	// GetImportState returns ErrorNotInitialized if there is no import state.
+	GetImportState() (is ImportState, err error)
 	SetImportState(ImportState) (err error)
+	// GetMaxRoundAccounted returns ErrorNotInitialized if there are no accounted rounds.
 	GetMaxRoundAccounted() (round uint64, err error)
+	// GetMaxRoundLoaded returns ErrorNotInitialized if there are no loaded rounds.
 	GetMaxRoundLoaded() (round uint64, err error)
 	GetSpecialAccounts() (SpecialAccounts, error)
 	GetDefaultFrozen() (defaultFrozen map[uint64]bool, err error)
 
-	// YieldTxns returns a channel that produces the whole transaction stream after some round forward
-	YieldTxns(ctx context.Context, prevRound int64) <-chan TxnRow
+	// YieldTxns returns a channel that produces the whole transaction stream starting at the specified round
+	YieldTxns(ctx context.Context, firstRound uint64) <-chan TxnRow
 
 	CommitRoundAccounting(updates RoundUpdates, round uint64, blockPtr *types.Block) (err error)
 
@@ -242,6 +110,7 @@ type IndexerDb interface {
 	Applications(ctx context.Context, filter *models.SearchForApplicationsParams) (<-chan ApplicationRow, uint64)
 
 	Health() (status Health, err error)
+	Reset() (err error)
 }
 
 // GetBlockOptions contains the options when requesting to load a block from the database.
@@ -293,18 +162,18 @@ type TransactionFilter struct {
 	OffsetGT   *uint64 // nil for no filter
 	SigType    string  // ["", "sig", "msig", "lsig"]
 	NotePrefix []byte
-	AlgosGT    uint64 // implictly filters on "pay" txns for Algos > this. This will be a slightly faster query than EffectiveAmountGt.
-	AlgosLT    uint64
+	AlgosGT    *uint64 // implictly filters on "pay" txns for Algos > this. This will be a slightly faster query than EffectiveAmountGT.
+	AlgosLT    *uint64
 	RekeyTo    *bool // nil for no filter
 
 	AssetID       uint64 // filter transactions relevant to an asset
-	AssetAmountGT uint64
-	AssetAmountLT uint64
+	AssetAmountGT *uint64
+	AssetAmountLT *uint64
 
 	ApplicationID uint64 // filter transactions relevant to an application
 
-	EffectiveAmountGt uint64 // Algo: Amount + CloseAmount > x
-	EffectiveAmountLt uint64 // Algo: Amount + CloseAmount < x
+	EffectiveAmountGT *uint64 // Algo: Amount + CloseAmount > x
+	EffectiveAmountLT *uint64 // Algo: Amount + CloseAmount < x
 
 	// pointer to last returned object of previous query
 	NextToken string
@@ -321,16 +190,16 @@ type AccountQueryOptions struct {
 	EqualToAuthAddr []byte
 
 	// Filter on accounts with current balance greater than x
-	AlgosGreaterThan uint64
+	AlgosGreaterThan *uint64
 	// Filter on accounts with current balance less than x.
-	AlgosLessThan uint64
+	AlgosLessThan *uint64
 
 	// HasAssetID, AssetGT, and AssetLT are implemented in Go code
 	// after data has returned from Postgres and thus are slightly
 	// less efficient. They will turn on IncludeAssetHoldings.
 	HasAssetID uint64
-	AssetGT    uint64
-	AssetLT    uint64
+	AssetGT    *uint64
+	AssetLT    *uint64
 
 	HasAppID uint64
 
@@ -384,8 +253,8 @@ type AssetRow struct {
 // AssetBalanceQuery is a parameter object with all of the asset balance filter options.
 type AssetBalanceQuery struct {
 	AssetID  uint64
-	AmountGT uint64 // only rows > this
-	AmountLT uint64 // only rows < this
+	AmountGT *uint64 // only rows > this
+	AmountLT *uint64 // only rows < this
 
 	// IncludeDeleted indicated whether to include deleted AssetHoldingss in the results.
 	IncludeDeleted bool
@@ -415,45 +284,13 @@ type ApplicationRow struct {
 	Error       error
 }
 
-type dummyFactory struct {
-}
-
-// Name is part of the IndexerFactory interface.
-func (df dummyFactory) Name() string {
-	return "dummy"
-}
-
-// Build is part of the IndexerFactory interface.
-func (df dummyFactory) Build(arg string, opts *IndexerDbOptions, log *log.Logger) (IndexerDb, error) {
-	return &dummyIndexerDb{log: log}, nil
-}
-
-// This layer of indirection allows for different db integrations to be compiled in or compiled out by `go build --tags ...`
-var indexerFactories map[string]IndexerFactory
-
-func init() {
-	indexerFactories = make(map[string]IndexerFactory)
-	RegisterFactory("dummy", &dummyFactory{})
-}
-
 // IndexerDbOptions are the options common to all indexer backends.
 type IndexerDbOptions struct {
 	ReadOnly bool
-}
 
-// RegisterFactory is used by IndexerDb implementations to register their implementations. This mechanism allows
-// for loose coupling between the configuration and the implementation. It is extremely similar to the way sql.DB
-// driver's are configured and used.
-func RegisterFactory(name string, factory IndexerFactory) {
-	indexerFactories[name] = factory
-}
-
-// IndexerDbByName is used to construct an IndexerDb object by name.
-func IndexerDbByName(name, arg string, opts *IndexerDbOptions, log *log.Logger) (IndexerDb, error) {
-	if val, ok := indexerFactories[name]; ok {
-		return val.Build(arg, opts, log)
-	}
-	return nil, fmt.Errorf("no IndexerDb factory for %s", name)
+	// NoMigrate indicates to not run any migrations.
+	// Should probably only be used by the `reset` subcommand.
+	NoMigrate bool
 }
 
 // AssetUpdate is used by the accounting and IndexerDb implementations to share modifications in a block.
@@ -564,13 +401,13 @@ type AppDelta struct {
 	AddrIndex    uint64 // 0=Sender, otherwise stxn.Txn.Accounts[i-1]
 	Creator      []byte
 	Delta        types.StateDelta
-	OnCompletion atypes.OnCompletion
+	OnCompletion sdk_types.OnCompletion
 
 	// AppParams settings coppied from Txn, only for AppGlobalDeltas
-	ApprovalProgram   []byte             `codec:"approv"`
-	ClearStateProgram []byte             `codec:"clearp"`
-	LocalStateSchema  atypes.StateSchema `codec:"lsch"`
-	GlobalStateSchema atypes.StateSchema `codec:"gsch"`
+	ApprovalProgram   []byte                `codec:"approv"`
+	ClearStateProgram []byte                `codec:"clearp"`
+	LocalStateSchema  sdk_types.StateSchema `codec:"lsch"`
+	GlobalStateSchema sdk_types.StateSchema `codec:"gsch"`
 }
 
 // String is part of the Stringer interface.
@@ -612,12 +449,12 @@ type StateDelta struct {
 
 // AppReverseDelta extra data attached to transactions relating to applications
 type AppReverseDelta struct {
-	Delta             []StateDelta        `codec:"d,omitempty"`
-	OnCompletion      atypes.OnCompletion `codec:"oc,omitempty"`
-	ApprovalProgram   []byte              `codec:"approv,omitempty"`
-	ClearStateProgram []byte              `codec:"clearp,omitempty"`
-	LocalStateSchema  atypes.StateSchema  `codec:"lsch,omitempty"`
-	GlobalStateSchema atypes.StateSchema  `codec:"gsch,omitempty"`
+	Delta             []StateDelta           `codec:"d,omitempty"`
+	OnCompletion      sdk_types.OnCompletion `codec:"oc,omitempty"`
+	ApprovalProgram   []byte                 `codec:"approv,omitempty"`
+	ClearStateProgram []byte                 `codec:"clearp,omitempty"`
+	LocalStateSchema  sdk_types.StateSchema  `codec:"lsch,omitempty"`
+	GlobalStateSchema sdk_types.StateSchema  `codec:"gsch,omitempty"`
 }
 
 // SetDelta adds delta values to the AppReverseDelta object.
@@ -655,3 +492,18 @@ type SpecialAccounts struct {
 type ImportState struct {
 	AccountRound int64 `codec:"account_round"`
 }
+
+// UpdateFilter is used by some functions to filter how an update is done.
+type UpdateFilter struct {
+	// StartRound only include transactions confirmed at this round or later.
+	StartRound uint64
+
+	// RoundLimit only process this many rounds of transactions.
+	RoundLimit *int
+
+	// MaxRound stop processing after this round
+	MaxRound uint64
+
+	// Address only process transactions which modify this account.
+	Address *types.Address
+}
diff --git a/idb/idb_factory.go b/idb/idb_factory.go
new file mode 100644
index 0000000..b67a30a
--- /dev/null
+++ b/idb/idb_factory.go
@@ -0,0 +1,35 @@
+package idb
+
+import (
+	"fmt"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// IndexerDbFactory is used to install an IndexerDb implementation.
+type IndexerDbFactory interface {
+	Name() string
+	Build(arg string, opts IndexerDbOptions, log *log.Logger) (IndexerDb, error)
+}
+
+// This layer of indirection allows for different db integrations to be compiled in or compiled out by `go build --tags ...`
+var indexerFactories map[string]IndexerDbFactory
+
+// RegisterFactory is used by IndexerDb implementations to register their implementations. This mechanism allows
+// for loose coupling between the configuration and the implementation. It is extremely similar to the way sql.DB
+// driver's are configured and used.
+func RegisterFactory(name string, factory IndexerDbFactory) {
+	indexerFactories[name] = factory
+}
+
+// IndexerDbByName is used to construct an IndexerDb object by name.
+func IndexerDbByName(name, arg string, opts IndexerDbOptions, log *log.Logger) (IndexerDb, error) {
+	if val, ok := indexerFactories[name]; ok {
+		return val.Build(arg, opts, log)
+	}
+	return nil, fmt.Errorf("no IndexerDb factory for %s", name)
+}
+
+func init() {
+	indexerFactories = make(map[string]IndexerDbFactory)
+}
diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go
index a729957..d2dbe26 100644
--- a/idb/mocks/IndexerDb.go
+++ b/idb/mocks/IndexerDb.go
@@ -32,27 +32,6 @@ func (_m *IndexerDb) AddTransaction(round uint64, intra int, txtypeenum int, ass
 	return r0
 }
 
-// AlreadyImported provides a mock function with given fields: path
-func (_m *IndexerDb) AlreadyImported(path string) (bool, error) {
-	ret := _m.Called(path)
-
-	var r0 bool
-	if rf, ok := ret.Get(0).(func(string) bool); ok {
-		r0 = rf(path)
-	} else {
-		r0 = ret.Get(0).(bool)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(string) error); ok {
-		r1 = rf(path)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
 // Applications provides a mock function with given fields: ctx, filter
 func (_m *IndexerDb) Applications(ctx context.Context, filter *generated.SearchForApplicationsParams) (<-chan idb.ApplicationRow, uint64) {
 	ret := _m.Called(ctx, filter)
@@ -227,16 +206,14 @@ func (_m *IndexerDb) GetDefaultFrozen() (map[uint64]bool, error) {
 }
 
 // GetImportState provides a mock function with given fields:
-func (_m *IndexerDb) GetImportState() (*idb.ImportState, error) {
+func (_m *IndexerDb) GetImportState() (idb.ImportState, error) {
 	ret := _m.Called()
 
-	var r0 *idb.ImportState
-	if rf, ok := ret.Get(0).(func() *idb.ImportState); ok {
+	var r0 idb.ImportState
+	if rf, ok := ret.Get(0).(func() idb.ImportState); ok {
 		r0 = rf()
 	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*idb.ImportState)
-		}
+		r0 = ret.Get(0).(idb.ImportState)
 	}
 
 	var r1 error
@@ -291,27 +268,6 @@ func (_m *IndexerDb) GetMaxRoundLoaded() (uint64, error) {
 	return r0, r1
 }
 
-// GetProto provides a mock function with given fields: version
-func (_m *IndexerDb) GetProto(version string) (types.ConsensusParams, error) {
-	ret := _m.Called(version)
-
-	var r0 types.ConsensusParams
-	if rf, ok := ret.Get(0).(func(string) types.ConsensusParams); ok {
-		r0 = rf(version)
-	} else {
-		r0 = ret.Get(0).(types.ConsensusParams)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(string) error); ok {
-		r1 = rf(version)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
 // GetSpecialAccounts provides a mock function with given fields:
 func (_m *IndexerDb) GetSpecialAccounts() (idb.SpecialAccounts, error) {
 	ret := _m.Called()
@@ -368,13 +324,13 @@ func (_m *IndexerDb) LoadGenesis(genesis types.Genesis) error {
 	return r0
 }
 
-// MarkImported provides a mock function with given fields: path
-func (_m *IndexerDb) MarkImported(path string) error {
-	ret := _m.Called(path)
+// Reset provides a mock function with given fields:
+func (_m *IndexerDb) Reset() error {
+	ret := _m.Called()
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(string) error); ok {
-		r0 = rf(path)
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -396,20 +352,6 @@ func (_m *IndexerDb) SetImportState(_a0 idb.ImportState) error {
 	return r0
 }
 
-// SetProto provides a mock function with given fields: version, proto
-func (_m *IndexerDb) SetProto(version string, proto types.ConsensusParams) error {
-	ret := _m.Called(version, proto)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(string, types.ConsensusParams) error); ok {
-		r0 = rf(version, proto)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
 // StartBlock provides a mock function with given fields:
 func (_m *IndexerDb) StartBlock() error {
 	ret := _m.Called()
@@ -447,13 +389,13 @@ func (_m *IndexerDb) Transactions(ctx context.Context, tf idb.TransactionFilter)
 	return r0, r1
 }
 
-// YieldTxns provides a mock function with given fields: ctx, prevRound
-func (_m *IndexerDb) YieldTxns(ctx context.Context, prevRound int64) <-chan idb.TxnRow {
-	ret := _m.Called(ctx, prevRound)
+// YieldTxns provides a mock function with given fields: ctx, firstRound
+func (_m *IndexerDb) YieldTxns(ctx context.Context, firstRound uint64) <-chan idb.TxnRow {
+	ret := _m.Called(ctx, firstRound)
 
 	var r0 <-chan idb.TxnRow
-	if rf, ok := ret.Get(0).(func(context.Context, int64) <-chan idb.TxnRow); ok {
-		r0 = rf(ctx, prevRound)
+	if rf, ok := ret.Get(0).(func(context.Context, uint64) <-chan idb.TxnRow); ok {
+		r0 = rf(ctx, firstRound)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(<-chan idb.TxnRow)
diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go
index 5d8423d..3ff8c53 100644
--- a/idb/postgres/postgres.go
+++ b/idb/postgres/postgres.go
@@ -3,8 +3,8 @@
 
 package postgres
 
-// import text to contstant setup_postgres_sql
-//go:generate go run ../../cmd/texttosource/main.go postgres setup_postgres.sql
+// import text to constants setup_postgres_sql reset_sql
+//go:generate go run ../../cmd/texttosource/main.go postgres setup_postgres.sql reset.sql
 
 import (
 	"bytes"
@@ -24,10 +24,10 @@ import (
 	"github.com/algorand/go-algorand-sdk/crypto"
 	"github.com/algorand/go-algorand-sdk/encoding/json"
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	// Load the postgres sql.DB implementation
-	_ "github.com/lib/pq"
+	"github.com/lib/pq"
 	log "github.com/sirupsen/logrus"
 
 	models "github.com/algorand/indexer/api/generated/v2"
@@ -45,17 +45,14 @@ var serializable = sql.TxOptions{Isolation: sql.LevelSerializable} // be a real
 var readonlyRepeatableRead = sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}
 
 // OpenPostgres is available for creating test instances of postgres.IndexerDb
-func OpenPostgres(connection string, opts *idb.IndexerDbOptions, log *log.Logger) (pdb *IndexerDb, err error) {
+func OpenPostgres(connection string, opts idb.IndexerDbOptions, log *log.Logger) (pdb *IndexerDb, err error) {
 	db, err := sql.Open("postgres", connection)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("connecting to postgres: %v", err)
 	}
 
 	if strings.Contains(connection, "readonly") {
-		if opts == nil {
-			opts = &idb.IndexerDbOptions{}
-		}
 		opts.ReadOnly = true
 	}
 
@@ -63,11 +60,10 @@ func OpenPostgres(connection string, opts *idb.IndexerDbOptions, log *log.Logger
 }
 
 // Allow tests to inject a DB
-func openPostgres(db *sql.DB, opts *idb.IndexerDbOptions, logger *log.Logger) (pdb *IndexerDb, err error) {
+func openPostgres(db *sql.DB, opts idb.IndexerDbOptions, logger *log.Logger) (pdb *IndexerDb, err error) {
 	pdb = &IndexerDb{
-		log:        logger,
-		db:         db,
-		protoCache: make(map[string]types.ConsensusParams, 20),
+		log: logger,
+		db:  db,
 	}
 
 	if pdb.log == nil {
@@ -78,9 +74,11 @@ func openPostgres(db *sql.DB, opts *idb.IndexerDbOptions, logger *log.Logger) (p
 	}
 
 	// e.g. a user named "readonly" is in the connection string
-	readonly := (opts != nil) && opts.ReadOnly
-	if !readonly {
-		err = pdb.init()
+	if !opts.ReadOnly {
+		err = pdb.init(opts)
+		if err != nil {
+			return nil, fmt.Errorf("initializing postgres: %v", err)
+		}
 	}
 	return
 }
@@ -92,21 +90,45 @@ type IndexerDb struct {
 	db *sql.DB
 
 	// state for StartBlock/AddTransaction/CommitBlock
-	tx        *sql.Tx
-	addtx     *sql.Stmt
-	addtxpart *sql.Stmt
-
 	txrows  [][]interface{}
 	txprows [][]interface{}
 
-	protoCache map[string]types.ConsensusParams
-
 	migration *migration.Migration
 
 	accountingLock sync.Mutex
 }
 
-func (db *IndexerDb) init() (err error) {
+// A helper function that retries the function `f` in case the database transaction in it
+// fails due to a serialization error. `f` is provided context `ctx` and a transaction created
+// using this context and `opts`. `f` takes ownership of the transaction and must either call
+// sql.Tx.Rollback() or sql.Tx.Commit(). In the second case, `f` must return an error which
+// contains the error returned by sql.Tx.Commit(). The easiest way is to just return the result
+// of sql.Tx.Commit().
+func (db *IndexerDb) txWithRetry(ctx context.Context, opts sql.TxOptions, f func(context.Context, *sql.Tx) error) error {
+	count := 0
+	for {
+		tx, err := db.db.BeginTx(ctx, &opts)
+		if err != nil {
+			return err
+		}
+
+		err = f(ctx, tx)
+
+		// If not serialization error.
+		var pqerr *pq.Error
+		if !errors.As(err, &pqerr) || (pqerr.Code != "40001") {
+			if count > 0 {
+				db.log.Printf("transaction was retried %d times", count)
+			}
+			return err
+		}
+
+		count++
+		db.log.Printf("retrying transaction, count: %d", count)
+	}
+}
+
+func (db *IndexerDb) init(opts idb.IndexerDbOptions) (err error) {
 	accountingStateJSON, _ := db.getMetastate(stateMetastateKey)
 	hasAccounting := len(accountingStateJSON) > 0
 	migrationStateJSON, _ := db.getMetastate(migrationMetastateKey)
@@ -114,7 +136,7 @@ func (db *IndexerDb) init() (err error) {
 
 	db.GetSpecialAccounts()
 
-	if hasMigration || hasAccounting {
+	if (hasMigration || hasAccounting) && !opts.NoMigrate {
 		// see postgres_migrations.go
 		return db.runAvailableMigrations(migrationStateJSON)
 	}
@@ -122,25 +144,26 @@ func (db *IndexerDb) init() (err error) {
 	// new database, run setup
 	_, err = db.db.Exec(setup_postgres_sql)
 	if err != nil {
-		return
+		return fmt.Errorf("unable to setup postgres: %v", err)
 	}
 
 	err = db.markMigrationsAsDone()
-	return
-}
+	if err != nil {
+		return fmt.Errorf("unable to confirm migration: %v", err)
+	}
 
-// AlreadyImported is part of idb.IndexerDB
-func (db *IndexerDb) AlreadyImported(path string) (imported bool, err error) {
-	row := db.db.QueryRow(`SELECT COUNT(path) FROM imported WHERE path = $1`, path)
-	numpath := 0
-	err = row.Scan(&numpath)
-	return numpath == 1, err
+	return nil
 }
 
-// MarkImported is part of idb.IndexerDB
-func (db *IndexerDb) MarkImported(path string) (err error) {
-	_, err = db.db.Exec(`INSERT INTO imported (path) VALUES ($1)`, path)
-	return err
+// Reset is part of idb.IndexerDB
+func (db *IndexerDb) Reset() (err error) {
+	// new database, run setup
+	_, err = db.db.Exec(reset_sql)
+	if err != nil {
+		return fmt.Errorf("db reset failed, %v", err)
+	}
+	db.log.Debugf("reset.sql done")
+	return
 }
 
 // StartBlock is part of idb.IndexerDB
@@ -188,13 +211,9 @@ func (db *IndexerDb) AddTransaction(round uint64, intra int, txtypeenum int, ass
 	return nil
 }
 
-// CommitBlock is part of idb.IndexerDB
-func (db *IndexerDb) CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error {
-	tx, err := db.db.BeginTx(context.Background(), &serializable)
-	if err != nil {
-		return fmt.Errorf("BeginTx %v", err)
-	}
+func (db *IndexerDb) commitBlock(tx *sql.Tx, round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error {
 	defer tx.Rollback() // ignored if already committed
+
 	addtx, err := tx.Prepare(`COPY txn (round, intra, typeenum, asset, txid, txnbytes, txn) FROM STDIN`)
 	if err != nil {
 		return fmt.Errorf("COPY txn %v", err)
@@ -268,25 +287,23 @@ func (db *IndexerDb) CommitBlock(round uint64, timestamp int64, rewardslevel uin
 		return fmt.Errorf("put block_header %v    %#v", err, err)
 	}
 
-	err = tx.Commit()
+	return tx.Commit()
+}
+
+// CommitBlock is part of idb.IndexerDB
+func (db *IndexerDb) CommitBlock(round uint64, timestamp int64, rewardslevel uint64, headerbytes []byte) error {
+	f := func(ctx context.Context, tx *sql.Tx) error {
+		return db.commitBlock(tx, round, timestamp, rewardslevel, headerbytes)
+	}
+	err := db.txWithRetry(context.Background(), serializable, f)
+
 	db.txrows = nil
 	db.txprows = nil
-	if err != nil {
-		return fmt.Errorf("on commit, %v", err)
-	}
-	return err
-}
 
-// GetAsset return AssetParams about an asset
-func (db *IndexerDb) GetAsset(assetid uint64) (asset types.AssetParams, err error) {
-	row := db.db.QueryRow(`SELECT params FROM asset WHERE index = $1`, assetid)
-	var assetjson string
-	err = row.Scan(&assetjson)
 	if err != nil {
-		return
+		return fmt.Errorf("CommitBlock(): %v", err)
 	}
-	err = json.Decode([]byte(assetjson), &asset)
-	return
+	return nil
 }
 
 // GetDefaultFrozen get {assetid:default frozen, ...} for all assets, needed by accounting.
@@ -324,7 +341,10 @@ func (db *IndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
 
 	total := uint64(0)
 	for ai, alloc := range genesis.Allocation {
-		addr, err := atypes.DecodeAddress(alloc.Address)
+		addr, err := sdk_types.DecodeAddress(alloc.Address)
+		if err != nil {
+			return nil
+		}
 		if len(alloc.State.AssetParams) > 0 || len(alloc.State.Assets) > 0 {
 			return fmt.Errorf("genesis account[%d] has unhandled asset", ai)
 		}
@@ -346,35 +366,6 @@ func (db *IndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
 
 }
 
-// SetProto is part of idb.IndexerDB
-func (db *IndexerDb) SetProto(version string, proto types.ConsensusParams) (err error) {
-	pj := idb.JSONOneLine(proto)
-	if err != nil {
-		return err
-	}
-	_, err = db.db.Exec(`INSERT INTO protocol (version, proto) VALUES ($1, $2) ON CONFLICT (version) DO UPDATE SET proto = EXCLUDED.proto`, version, pj)
-	return err
-}
-
-// GetProto is part of idb.IndexerDB
-func (db *IndexerDb) GetProto(version string) (proto types.ConsensusParams, err error) {
-	proto, hit := db.protoCache[version]
-	if hit {
-		return
-	}
-	row := db.db.QueryRow(`SELECT proto FROM protocol WHERE version = $1`, version)
-	var protostr string
-	err = row.Scan(&protostr)
-	if err != nil {
-		return
-	}
-	err = json.Decode([]byte(protostr), &proto)
-	if err == nil {
-		db.protoCache[version] = proto
-	}
-	return
-}
-
 func (db *IndexerDb) getMetastate(key string) (jsonStrValue string, err error) {
 	row := db.db.QueryRow(`SELECT v FROM metastate WHERE k = $1`, key)
 	err = row.Scan(&jsonStrValue)
@@ -395,12 +386,12 @@ func (db *IndexerDb) setMetastate(key, jsonStrValue string) (err error) {
 }
 
 // GetImportState is part of idb.IndexerDB
-func (db *IndexerDb) GetImportState() (state *idb.ImportState, err error) {
+func (db *IndexerDb) GetImportState() (state idb.ImportState, err error) {
 	var importStateJSON string
 	importStateJSON, err = db.getMetastate(stateMetastateKey)
 	if err == sql.ErrNoRows || importStateJSON == "" {
 		// no previous state, ok
-		err = nil
+		err = idb.ErrorNotInitialized
 		return
 	} else if err != nil {
 		err = fmt.Errorf("unable to get import state: %v", err)
@@ -421,7 +412,7 @@ func (db *IndexerDb) SetImportState(state idb.ImportState) (err error) {
 
 // If `tx` is null, make a standalone query.
 func (db *IndexerDb) getMaxRoundAccounted(tx *sql.Tx) (round uint64, err error) {
-	query := `select coalesce((v->>'account_round')::bigint, 0) from metastate where k = 'state'`
+	query := `select v->>'account_round' from metastate where k = 'state'`
 
 	var row *sql.Row
 	if tx == nil {
@@ -430,12 +421,23 @@ func (db *IndexerDb) getMaxRoundAccounted(tx *sql.Tx) (round uint64, err error)
 		row = tx.QueryRow(query)
 	}
 
-	err = row.Scan(&round)
+	var nullableRound sql.NullInt64
+	err = row.Scan(&nullableRound)
 	if err == sql.ErrNoRows {
-		err = nil
+		err = idb.ErrorNotInitialized
 		round = 0
 	}
 
+	if err != nil {
+		return
+	}
+
+	if nullableRound.Valid {
+		round = uint64(nullableRound.Int64)
+	} else {
+		err = idb.ErrorNotInitialized
+	}
+
 	return
 }
 
@@ -450,10 +452,17 @@ func (db *IndexerDb) GetMaxRoundLoaded() (round uint64, err error) {
 	round = 0
 	row := db.db.QueryRow(`SELECT max(round) FROM block_header`)
 	err = row.Scan(&nullableRound)
-	if err == nil && nullableRound.Valid {
-		round = uint64(nullableRound.Int64)
+
+	if err == sql.ErrNoRows || !nullableRound.Valid {
+		err = idb.ErrorNotInitialized
+		return
+	}
+
+	if err != nil {
+		return
 	}
 
+	round = uint64(nullableRound.Int64)
 	return
 }
 
@@ -492,7 +501,6 @@ func (db *IndexerDb) yieldTxnsThread(ctx context.Context, rows *sql.Rows, result
 				row.Error = err
 				results <- row
 				rows.Close()
-				close(results)
 				return
 			}
 
@@ -511,7 +519,6 @@ func (db *IndexerDb) yieldTxnsThread(ctx context.Context, rows *sql.Rows, result
 			row.Error = err
 			results <- row
 			rows.Close()
-			close(results)
 			return
 		}
 		rows.Close()
@@ -544,13 +551,11 @@ func (db *IndexerDb) yieldTxnsThread(ctx context.Context, rows *sql.Rows, result
 				if err != nil {
 					row.Error = fmt.Errorf("%d:%d decode txn extra, %v", row.Round, row.Intra, err)
 					results <- row
-					close(results)
 					return
 				}
 			}
 			select {
 			case <-ctx.Done():
-				close(results)
 				return
 			case results <- row:
 			}
@@ -565,19 +570,21 @@ func (db *IndexerDb) yieldTxnsThread(ctx context.Context, rows *sql.Rows, result
 			}
 		}
 	}
-	close(results)
 }
 
 // YieldTxns is part of idb.IndexerDB
-func (db *IndexerDb) YieldTxns(ctx context.Context, prevRound int64) <-chan idb.TxnRow {
+func (db *IndexerDb) YieldTxns(ctx context.Context, firstRound uint64) <-chan idb.TxnRow {
 	results := make(chan idb.TxnRow, 1)
-	rows, err := db.db.QueryContext(ctx, yieldTxnQuery, prevRound)
+	rows, err := db.db.QueryContext(ctx, yieldTxnQuery, int64(firstRound)-1)
 	if err != nil {
 		results <- idb.TxnRow{Error: err}
 		close(results)
 		return results
 	}
-	go db.yieldTxnsThread(ctx, rows, results)
+	go func() {
+		db.yieldTxnsThread(ctx, rows, results)
+		close(results)
+	}()
 	return results
 }
 
@@ -601,7 +608,7 @@ type StateSchema struct {
 	NumByteSlice uint64 `codec:"nbs"`
 }
 
-func (ss *StateSchema) fromBlock(x atypes.StateSchema) {
+func (ss *StateSchema) fromBlock(x sdk_types.StateSchema) {
 	if x.NumUint != 0 || x.NumByteSlice != 0 {
 		ss.NumUint = x.NumUint
 		ss.NumByteSlice = x.NumByteSlice
@@ -814,18 +821,13 @@ func setDirtyAppLocalState(dirty []inmemAppLocalState, x inmemAppLocalState) []i
 	return append(dirty, x)
 }
 
-// CommitRoundAccounting is part of idb.IndexerDB
-func (db *IndexerDb) CommitRoundAccounting(updates idb.RoundUpdates, round uint64, blockPtr *types.Block) (err error) {
+func (db *IndexerDb) commitRoundAccounting(tx *sql.Tx, updates idb.RoundUpdates, round uint64, blockPtr *types.Block) (err error) {
+	defer tx.Rollback() // ignored if .Commit() first
+
 	db.accountingLock.Lock()
 	defer db.accountingLock.Unlock()
 
 	any := false
-	tx, err := db.db.BeginTx(context.Background(), &serializable)
-	if err != nil {
-		return
-	}
-	defer tx.Rollback() // ignored if .Commit() first
-
 	if len(updates.AlgoUpdates) > 0 {
 		any = true
 		// account_data json is only used on account creation, otherwise the account data jsonb field is updated from the delta
@@ -1045,7 +1047,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 							if err != nil {
 								return fmt.Errorf("get acfg %d, %v", au.AssetID, err)
 							}
-							var old atypes.AssetParams
+							var old sdk_types.AssetParams
 							err = json.Decode(paramjson, &old)
 							if err != nil {
 								return fmt.Errorf("bad acgf json %d, %v", au.AssetID, err)
@@ -1152,7 +1154,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 				}
 			}
 			reverseDeltas = append(reverseDeltas, []interface{}{idb.JSONOneLine(reverseDelta), adelta.Round, adelta.Intra})
-			if adelta.OnCompletion == atypes.DeleteApplicationOC {
+			if adelta.OnCompletion == sdk_types.DeleteApplicationOC {
 				// clear content but leave row recording that it existed
 				state = AppParams{}
 				destroy[uint64(adelta.AppIndex)] = true
@@ -1215,7 +1217,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 		}
 
 		for _, ald := range updates.AppLocalDeltas {
-			if ald.OnCompletion == atypes.CloseOutOC || ald.OnCompletion == atypes.ClearStateOC {
+			if ald.OnCompletion == sdk_types.CloseOutOC || ald.OnCompletion == sdk_types.ClearStateOC {
 				droplocals = append(droplocals,
 					[]interface{}{ald.Address, ald.AppIndex, round},
 				)
@@ -1225,7 +1227,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 			if err != nil {
 				return err
 			}
-			if ald.OnCompletion == atypes.OptInOC {
+			if ald.OnCompletion == sdk_types.OptInOC {
 				row := getapp.QueryRow(ald.AppIndex)
 				var paramsjson []byte
 				err = row.Scan(&paramsjson)
@@ -1328,6 +1330,17 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 	return tx.Commit()
 }
 
+// CommitRoundAccounting is part of idb.IndexerDB
+func (db *IndexerDb) CommitRoundAccounting(updates idb.RoundUpdates, round uint64, blockPtr *types.Block) error {
+	f := func(ctx context.Context, tx *sql.Tx) (err error) {
+		return db.commitRoundAccounting(tx, updates, round, blockPtr)
+	}
+	if err := db.txWithRetry(context.Background(), serializable, f); err != nil {
+		return fmt.Errorf("CommitRoundAccounting(): %v", err)
+	}
+	return nil
+}
+
 // GetBlock is part of idb.IndexerDB
 func (db *IndexerDb) GetBlock(ctx context.Context, round uint64, options idb.GetBlockOptions) (block types.Block, transactions []idb.TxnRow, err error) {
 	tx, err := db.db.BeginTx(ctx, &readonlyRepeatableRead)
@@ -1361,7 +1374,10 @@ func (db *IndexerDb) GetBlock(ctx context.Context, round uint64, options idb.Get
 			return types.Block{}, nil, err
 		}
 
-		go db.yieldTxnsThreadSimple(ctx, rows, out, true, nil, nil)
+		go func() {
+			db.yieldTxnsThreadSimple(ctx, rows, out, nil, nil)
+			close(out)
+		}()
 
 		results := make([]idb.TxnRow, 0)
 		for txrow := range out {
@@ -1421,7 +1437,7 @@ func buildTransactionQuery(tf idb.TransactionFilter) (query string, whereArgs []
 				partNumber++
 			}
 			if tf.AddressRole&idb.AddressRoleFreeze != 0 {
-				roleparts = append(roleparts, fmt.Sprintf("t.txn -> 'txn' ->> 'afrz' = $%d", partNumber))
+				roleparts = append(roleparts, fmt.Sprintf("t.txn -> 'txn' ->> 'fadd' = $%d", partNumber))
 				whereArgs = append(whereArgs, addrBase64)
 				partNumber++
 			}
@@ -1468,14 +1484,14 @@ func buildTransactionQuery(tf idb.TransactionFilter) (query string, whereArgs []
 		whereArgs = append(whereArgs, creatableID)
 		partNumber++
 	}
-	if tf.AssetAmountGT != 0 {
+	if tf.AssetAmountGT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("(t.txn -> 'txn' -> 'aamt')::bigint > $%d", partNumber))
-		whereArgs = append(whereArgs, tf.AssetAmountGT)
+		whereArgs = append(whereArgs, *tf.AssetAmountGT)
 		partNumber++
 	}
-	if tf.AssetAmountLT != 0 {
+	if tf.AssetAmountLT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("(t.txn -> 'txn' -> 'aamt')::bigint < $%d", partNumber))
-		whereArgs = append(whereArgs, tf.AssetAmountLT)
+		whereArgs = append(whereArgs, *tf.AssetAmountLT)
 		partNumber++
 	}
 	if tf.TypeEnum != 0 {
@@ -1518,24 +1534,24 @@ func buildTransactionQuery(tf idb.TransactionFilter) (query string, whereArgs []
 		whereArgs = append(whereArgs, tf.NotePrefix)
 		partNumber++
 	}
-	if tf.AlgosGT != 0 {
+	if tf.AlgosGT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("(t.txn -> 'txn' -> 'amt')::bigint > $%d", partNumber))
-		whereArgs = append(whereArgs, tf.AlgosGT)
+		whereArgs = append(whereArgs, *tf.AlgosGT)
 		partNumber++
 	}
-	if tf.AlgosLT != 0 {
+	if tf.AlgosLT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("(t.txn -> 'txn' -> 'amt')::bigint < $%d", partNumber))
-		whereArgs = append(whereArgs, tf.AlgosLT)
+		whereArgs = append(whereArgs, *tf.AlgosLT)
 		partNumber++
 	}
-	if tf.EffectiveAmountGt != 0 {
+	if tf.EffectiveAmountGT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("((t.txn -> 'ca')::bigint + (t.txn -> 'txn' -> 'amt')::bigint) > $%d", partNumber))
-		whereArgs = append(whereArgs, tf.EffectiveAmountGt)
+		whereArgs = append(whereArgs, *tf.EffectiveAmountGT)
 		partNumber++
 	}
-	if tf.EffectiveAmountLt != 0 {
+	if tf.EffectiveAmountLT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("((t.txn -> 'ca')::bigint + (t.txn -> 'txn' -> 'amt')::bigint) < $%d", partNumber))
-		whereArgs = append(whereArgs, tf.EffectiveAmountLt)
+		whereArgs = append(whereArgs, *tf.EffectiveAmountLT)
 		partNumber++
 	}
 	if tf.RekeyTo != nil && (*tf.RekeyTo) {
@@ -1562,55 +1578,53 @@ func buildTransactionQuery(tf idb.TransactionFilter) (query string, whereArgs []
 	return
 }
 
-// Transactions is part of idb.IndexerDB
-func (db *IndexerDb) Transactions(ctx context.Context, tf idb.TransactionFilter) (<-chan idb.TxnRow, uint64) {
-	out := make(chan idb.TxnRow, 1)
+// This function blocks. `tx` must be non-nil.
+func (db *IndexerDb) yieldTxns(ctx context.Context, tx *sql.Tx, tf idb.TransactionFilter, out chan<- idb.TxnRow) {
+	if len(tf.NextToken) > 0 {
+		db.txnsWithNext(ctx, tx, tf, out)
+		return
+	}
 
-	tx, err := db.db.BeginTx(ctx, &readonlyRepeatableRead)
+	query, whereArgs, err := buildTransactionQuery(tf)
 	if err != nil {
+		err = fmt.Errorf("txn query err %v", err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
-		return out, 0
+		return
 	}
 
-	round, err := db.getMaxRoundAccounted(tx)
+	rows, err := tx.Query(query, whereArgs...)
 	if err != nil {
+		err = fmt.Errorf("txn query %#v err %v", query, err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
-		tx.Rollback()
-		return out, round
+		return
 	}
 
-	if len(tf.NextToken) > 0 {
-		go func() {
-			db.txnsWithNext(ctx, tx, tf, out)
-			tx.Rollback()
-		}()
-		return out, round
-	}
+	db.yieldTxnsThreadSimple(ctx, rows, out, nil, nil)
+}
 
-	query, whereArgs, err := buildTransactionQuery(tf)
+// Transactions is part of idb.IndexerDB
+func (db *IndexerDb) Transactions(ctx context.Context, tf idb.TransactionFilter) (<-chan idb.TxnRow, uint64) {
+	out := make(chan idb.TxnRow, 1)
+
+	tx, err := db.db.BeginTx(ctx, &readonlyRepeatableRead)
 	if err != nil {
-		err = fmt.Errorf("txn query err %v", err)
 		out <- idb.TxnRow{Error: err}
 		close(out)
-		tx.Rollback()
 		return out, 0
 	}
 
-	rows, err := tx.Query(query, whereArgs...)
+	round, err := db.getMaxRoundAccounted(tx)
 	if err != nil {
-		err = fmt.Errorf("txn query %#v err %v", query, err)
 		out <- idb.TxnRow{Error: err}
 		close(out)
-		tx.Rollback()
 		return out, round
 	}
 
 	go func() {
-		db.yieldTxnsThreadSimple(ctx, rows, out, true, nil, nil)
-		tx.Rollback()
+		db.yieldTxns(ctx, tx, tf, out)
+		close(out)
 	}()
+
 	return out, round
 }
 
@@ -1636,16 +1650,20 @@ func (db *IndexerDb) txTransactions(tx *sql.Tx, tf idb.TransactionFilter) <-chan
 		close(out)
 		return out
 	}
-	go db.yieldTxnsThreadSimple(context.Background(), rows, out, true, nil, nil)
+	go func() {
+		db.yieldTxnsThreadSimple(context.Background(), rows, out, nil, nil)
+		close(out)
+	}()
 	return out
 }
 
+// This function blocks. `tx` must be non-nil.
 func (db *IndexerDb) txnsWithNext(ctx context.Context, tx *sql.Tx, tf idb.TransactionFilter, out chan<- idb.TxnRow) {
 	nextround, nextintra32, err := idb.DecodeTxnRowNext(tf.NextToken)
 	nextintra := uint64(nextintra32)
 	if err != nil {
 		out <- idb.TxnRow{Error: err}
-		close(out)
+		return
 	}
 	origRound := tf.Round
 	origOLT := tf.OffsetLT
@@ -1653,7 +1671,6 @@ func (db *IndexerDb) txnsWithNext(ctx context.Context, tx *sql.Tx, tf idb.Transa
 	if tf.Address != nil {
 		// (round,intra) descending into the past
 		if nextround == 0 && nextintra == 0 {
-			close(out)
 			return
 		}
 		tf.Round = &nextround
@@ -1667,30 +1684,25 @@ func (db *IndexerDb) txnsWithNext(ctx context.Context, tx *sql.Tx, tf idb.Transa
 	if err != nil {
 		err = fmt.Errorf("txn query err %v", err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
 		return
 	}
 	rows, err := tx.Query(query, whereArgs...)
 	if err != nil {
 		err = fmt.Errorf("txn query %#v err %v", query, err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
 		return
 	}
 	count := int(0)
-	db.yieldTxnsThreadSimple(ctx, rows, out, false, &count, &err)
+	db.yieldTxnsThreadSimple(ctx, rows, out, &count, &err)
 	if err != nil {
-		close(out)
 		return
 	}
 	if uint64(count) >= tf.Limit {
-		close(out)
 		return
 	}
 	tf.Limit -= uint64(count)
 	select {
 	case <-ctx.Done():
-		close(out)
 		return
 	default:
 	}
@@ -1700,7 +1712,6 @@ func (db *IndexerDb) txnsWithNext(ctx context.Context, tx *sql.Tx, tf idb.Transa
 		tf.OffsetLT = origOLT
 		if nextround == 0 {
 			// NO second query
-			close(out)
 			return
 		}
 		tf.MaxRound = nextround - 1
@@ -1713,20 +1724,18 @@ func (db *IndexerDb) txnsWithNext(ctx context.Context, tx *sql.Tx, tf idb.Transa
 	if err != nil {
 		err = fmt.Errorf("txn query err %v", err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
 		return
 	}
 	rows, err = tx.Query(query, whereArgs...)
 	if err != nil {
 		err = fmt.Errorf("txn query %#v err %v", query, err)
 		out <- idb.TxnRow{Error: err}
-		close(out)
 		return
 	}
-	db.yieldTxnsThreadSimple(ctx, rows, out, true, nil, nil)
+	db.yieldTxnsThreadSimple(ctx, rows, out, nil, nil)
 }
 
-func (db *IndexerDb) yieldTxnsThreadSimple(ctx context.Context, rows *sql.Rows, results chan<- idb.TxnRow, doClose bool, countp *int, errp *error) {
+func (db *IndexerDb) yieldTxnsThreadSimple(ctx context.Context, rows *sql.Rows, results chan<- idb.TxnRow, countp *int, errp *error) {
 	count := 0
 	for rows.Next() {
 		var round uint64
@@ -1772,16 +1781,11 @@ func (db *IndexerDb) yieldTxnsThreadSimple(ctx context.Context, rows *sql.Rows,
 		}
 	}
 finish:
-	if doClose {
-		close(results)
-	}
 	if countp != nil {
 		*countp = count
 	}
 }
 
-const maxAccountsLimit = 1000
-
 var statusStrings = []string{"Offline", "Online", "NotParticipating"}
 
 const offlineStatusIdx = 0
@@ -1875,7 +1879,7 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 		}
 
 		var account models.Account
-		var aaddr atypes.Address
+		var aaddr sdk_types.Address
 		copy(aaddr[:], addr)
 		account.Address = aaddr.String()
 		account.Round = uint64(req.blockheader.Round)
@@ -1918,7 +1922,7 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 			}
 
 			if !ad.SpendingKey.IsZero() {
-				var spendingkey atypes.Address
+				var spendingkey sdk_types.Address
 				copy(spendingkey[:], ad.SpendingKey[:])
 				account.AuthAddr = stringPtr(spendingkey.String())
 			}
@@ -1928,7 +1932,7 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 			account.PendingRewards = 0
 		} else {
 			// TODO: pending rewards calculation doesn't belong in database layer (this is just the most covenient place which has all the data)
-			proto, err := db.GetProto(string(req.blockheader.CurrentProtocol))
+			proto, err := types.Protocol(string(req.blockheader.CurrentProtocol))
 			if err != nil {
 				err = fmt.Errorf("get protocol err (%s) %v", req.blockheader.CurrentProtocol, err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2249,11 +2253,9 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 		case req.out <- idb.AccountRow{Account: account}:
 			count++
 			if req.opts.Limit != 0 && count >= req.opts.Limit {
-				close(req.out)
 				return
 			}
 		case <-req.ctx.Done():
-			close(req.out)
 			return
 		}
 	}
@@ -2261,7 +2263,6 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 		err = fmt.Errorf("error reading rows: %v", err)
 		req.out <- idb.AccountRow{Error: err}
 	}
-	close(req.out)
 }
 
 func nullableInt64Ptr(x sql.NullInt64) *uint64 {
@@ -2278,6 +2279,13 @@ func nullableBoolPtr(x sql.NullBool) *bool {
 	return &x.Bool
 }
 
+func uintOrDefault(x *uint64) uint64 {
+	if x != nil {
+		return *x
+	}
+	return 0
+}
+
 func uint64Ptr(x uint64) *uint64 {
 	out := new(uint64)
 	*out = x
@@ -2318,8 +2326,6 @@ func baPtr(x []byte) *[]byte {
 	return out
 }
 
-var emptyString = ""
-
 func allZero(x []byte) bool {
 	for _, v := range x {
 		if v != 0 {
@@ -2338,18 +2344,6 @@ func addrStr(addr types.Address) *string {
 	return out
 }
 
-func bytesStr(addr []byte) *string {
-	if len(addr) == 0 {
-		return nil
-	}
-	if allZero(addr) {
-		return nil
-	}
-	out := new(string)
-	*out = b64(addr)
-	return out
-}
-
 type getAccountsRequest struct {
 	ctx         context.Context
 	opts        idb.AccountQueryOptions
@@ -2366,8 +2360,8 @@ func (db *IndexerDb) GetAccounts(ctx context.Context, opts idb.AccountQueryOptio
 
 	if opts.HasAssetID != 0 {
 		opts.IncludeAssetHoldings = true
-	} else if (opts.AssetGT != 0) || (opts.AssetLT != 0) {
-		err := fmt.Errorf("AssetGT=%d, AssetLT=%d, but HasAssetID=%d", opts.AssetGT, opts.AssetLT, opts.HasAssetID)
+	} else if (opts.AssetGT != nil) || (opts.AssetLT != nil) {
+		err := fmt.Errorf("AssetGT=%d, AssetLT=%d, but HasAssetID=%d", uintOrDefault(opts.AssetGT), uintOrDefault(opts.AssetLT), opts.HasAssetID)
 		out <- idb.AccountRow{Error: err}
 		close(out)
 		return out, 0
@@ -2433,6 +2427,7 @@ func (db *IndexerDb) GetAccounts(ctx context.Context, opts idb.AccountQueryOptio
 	}
 	go func() {
 		db.yieldAccountsThread(req)
+		close(req.out)
 		tx.Rollback()
 	}()
 	return out, round
@@ -2450,14 +2445,14 @@ func (db *IndexerDb) buildAccountQuery(opts idb.AccountQueryOptions) (query stri
 		aq := fmt.Sprintf("SELECT addr FROM account_asset WHERE assetid = $%d", partNumber)
 		whereArgs = append(whereArgs, opts.HasAssetID)
 		partNumber++
-		if opts.AssetGT != 0 {
+		if opts.AssetGT != nil {
 			aq += fmt.Sprintf(" AND amount > $%d", partNumber)
-			whereArgs = append(whereArgs, opts.AssetGT)
+			whereArgs = append(whereArgs, *opts.AssetGT)
 			partNumber++
 		}
-		if opts.AssetLT != 0 {
+		if opts.AssetLT != nil {
 			aq += fmt.Sprintf(" AND amount < $%d", partNumber)
-			whereArgs = append(whereArgs, opts.AssetLT)
+			whereArgs = append(whereArgs, *opts.AssetLT)
 			partNumber++
 		}
 		aq = "qasf AS (" + aq + ")"
@@ -2479,14 +2474,14 @@ func (db *IndexerDb) buildAccountQuery(opts idb.AccountQueryOptions) (query stri
 		whereArgs = append(whereArgs, opts.EqualToAddress)
 		partNumber++
 	}
-	if opts.AlgosGreaterThan != 0 {
+	if opts.AlgosGreaterThan != nil {
 		whereParts = append(whereParts, fmt.Sprintf("a.microalgos > $%d", partNumber))
-		whereArgs = append(whereArgs, opts.AlgosGreaterThan)
+		whereArgs = append(whereArgs, *opts.AlgosGreaterThan)
 		partNumber++
 	}
-	if opts.AlgosLessThan != 0 {
+	if opts.AlgosLessThan != nil {
 		whereParts = append(whereParts, fmt.Sprintf("a.microalgos < $%d", partNumber))
-		whereArgs = append(whereArgs, opts.AlgosLessThan)
+		whereArgs = append(whereArgs, *opts.AlgosLessThan)
 		partNumber++
 	}
 	if !opts.IncludeDeleted {
@@ -2639,6 +2634,7 @@ func (db *IndexerDb) Assets(ctx context.Context, filter idb.AssetsQuery) (<-chan
 	}
 	go func() {
 		db.yieldAssetsThread(ctx, filter, rows, out)
+		close(out)
 		tx.Rollback()
 	}()
 	return out, round
@@ -2675,7 +2671,6 @@ func (db *IndexerDb) yieldAssetsThread(ctx context.Context, filter idb.AssetsQue
 		}
 		select {
 		case <-ctx.Done():
-			close(out)
 			return
 		case out <- rec:
 		}
@@ -2683,7 +2678,6 @@ func (db *IndexerDb) yieldAssetsThread(ctx context.Context, filter idb.AssetsQue
 	if err := rows.Err(); err != nil {
 		out <- idb.AssetRow{Error: err}
 	}
-	close(out)
 }
 
 // AssetBalances is part of idb.IndexerDB
@@ -2697,14 +2691,14 @@ func (db *IndexerDb) AssetBalances(ctx context.Context, abq idb.AssetBalanceQuer
 		whereArgs = append(whereArgs, abq.AssetID)
 		partNumber++
 	}
-	if abq.AmountGT != 0 {
+	if abq.AmountGT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("aa.amount > $%d", partNumber))
-		whereArgs = append(whereArgs, abq.AmountGT)
+		whereArgs = append(whereArgs, *abq.AmountGT)
 		partNumber++
 	}
-	if abq.AmountLT != 0 {
+	if abq.AmountLT != nil {
 		whereParts = append(whereParts, fmt.Sprintf("aa.amount < $%d", partNumber))
-		whereArgs = append(whereArgs, abq.AmountLT)
+		whereArgs = append(whereArgs, *abq.AmountLT)
 		partNumber++
 	}
 	if len(abq.PrevAddress) != 0 {
@@ -2752,6 +2746,7 @@ func (db *IndexerDb) AssetBalances(ctx context.Context, abq idb.AssetBalanceQuer
 	}
 	go func() {
 		db.yieldAssetBalanceThread(ctx, rows, out)
+		close(out)
 		tx.Rollback()
 	}()
 	return out, round
@@ -2782,7 +2777,6 @@ func (db *IndexerDb) yieldAssetBalanceThread(ctx context.Context, rows *sql.Rows
 		}
 		select {
 		case <-ctx.Done():
-			close(out)
 			return
 		case out <- rec:
 		}
@@ -2790,7 +2784,6 @@ func (db *IndexerDb) yieldAssetBalanceThread(ctx context.Context, rows *sql.Rows
 	if err := rows.Err(); err != nil {
 		out <- idb.AssetBalanceRow{Error: err}
 	}
-	close(out)
 }
 
 // Applications is part of idb.IndexerDB
@@ -2855,6 +2848,7 @@ func (db *IndexerDb) Applications(ctx context.Context, filter *models.SearchForA
 
 	go func() {
 		db.yieldApplicationsThread(ctx, rows, out)
+		close(out)
 		tx.Rollback()
 	}()
 	return out, round
@@ -2889,7 +2883,7 @@ func (db *IndexerDb) yieldApplicationsThread(ctx context.Context, rows *sql.Rows
 		rec.Application.Params.ClearStateProgram = ap.ClearStateProgram
 		rec.Application.Params.Creator = new(string)
 
-		var aaddr atypes.Address
+		var aaddr sdk_types.Address
 		copy(aaddr[:], creator)
 		rec.Application.Params.Creator = new(string)
 		*(rec.Application.Params.Creator) = aaddr.String()
@@ -2907,7 +2901,6 @@ func (db *IndexerDb) yieldApplicationsThread(ctx context.Context, rows *sql.Rows
 	if err := rows.Err(); err != nil {
 		out <- idb.ApplicationRow{Error: err}
 	}
-	close(out)
 }
 
 // Health is part of idb.IndexerDB
@@ -2944,6 +2937,13 @@ func (db *IndexerDb) Health() (idb.Health, error) {
 	data["migration-required"] = migrationRequired
 
 	round, err := db.GetMaxRoundAccounted()
+
+	// We'll just have to set the round to 0
+	if err == idb.ErrorNotInitialized {
+		err = nil
+		round = 0
+	}
+
 	return idb.Health{
 		Data:        &data,
 		Round:       round,
@@ -2985,17 +2985,3 @@ func (db *IndexerDb) GetSpecialAccounts() (accounts idb.SpecialAccounts, err err
 	}
 	return
 }
-
-type postgresFactory struct {
-}
-
-func (df postgresFactory) Name() string {
-	return "postgres"
-}
-func (df postgresFactory) Build(arg string, opts *idb.IndexerDbOptions, log *log.Logger) (idb.IndexerDb, error) {
-	return OpenPostgres(arg, opts, log)
-}
-
-func init() {
-	idb.RegisterFactory("postgres", &postgresFactory{})
-}
diff --git a/idb/postgres/postgres_factory.go b/idb/postgres/postgres_factory.go
new file mode 100644
index 0000000..59f950a
--- /dev/null
+++ b/idb/postgres/postgres_factory.go
@@ -0,0 +1,22 @@
+package postgres
+
+import (
+	log "github.com/sirupsen/logrus"
+
+	"github.com/algorand/indexer/idb"
+)
+
+type postgresFactory struct {
+}
+
+func (df postgresFactory) Name() string {
+	return "postgres"
+}
+
+func (df postgresFactory) Build(arg string, opts idb.IndexerDbOptions, log *log.Logger) (idb.IndexerDb, error) {
+	return OpenPostgres(arg, opts, log)
+}
+
+func init() {
+	idb.RegisterFactory("postgres", &postgresFactory{})
+}
diff --git a/idb/postgres/postgres_migrations.go b/idb/postgres/postgres_migrations.go
index 7bf9d96..5d811a5 100644
--- a/idb/postgres/postgres_migrations.go
+++ b/idb/postgres/postgres_migrations.go
@@ -49,6 +49,10 @@ func init() {
 		{m9TxnJSONEncoding, false, "some txn JSON encodings need app keys base64 encoded"},
 		{m10SpecialAccountCleanup, false, "The initial m7 implementation would miss special accounts."},
 		{m11AssetHoldingFrozen, false, "Fix asset holding freeze states."},
+
+		// Migrations for a next release
+		{FixFreezeLookupMigration, false, "Fix search by asset freeze address."},
+		{ClearAccountDataMigration, false, "clear account data for accounts that have been closed"},
 	}
 
 	// Verify ensure the constant is pointing to the right index
@@ -110,17 +114,27 @@ func needsMigration(state MigrationState) bool {
 	return state.NextMigration < len(migrations)
 }
 
-// upsertMigrationState updates the migration state, and optionally increments the next counter.
-func upsertMigrationState(tx *sql.Tx, state *MigrationState, incrementNextMigration bool) (err error) {
+// upsertMigrationStateTx updates the migration state, and optionally increments the next counter with an existing
+// transaction.
+func upsertMigrationStateTx(tx *sql.Tx, state *MigrationState, incrementNextMigration bool) (err error) {
 	if incrementNextMigration {
 		state.NextMigration++
 	}
 	migrationStateJSON := idb.JSONOneLine(state)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
-	if err != nil {
-		return fmt.Errorf("m10 meta error: %v", err)
+
+	return err
+}
+
+// upsertMigrationState updates the migration state, and optionally increments the next counter.
+func upsertMigrationState(db *IndexerDb, state *MigrationState, incrementNextMigration bool) (err error) {
+	if incrementNextMigration {
+		state.NextMigration++
 	}
-	return
+	migrationStateJSON := idb.JSONOneLine(state)
+	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
+
+	return err
 }
 
 func (db *IndexerDb) runAvailableMigrations(migrationStateJSON string) (err error) {
@@ -385,7 +399,10 @@ func m3acfgFixAsyncInner(db *IndexerDb, state *MigrationState, assetIds []int64)
 func m6RewardsAndDatesPart1(db *IndexerDb, state *MigrationState) error {
 	// Cache the round in the migration metastate
 	round, err := db.GetMaxRoundAccounted()
-	if err != nil {
+	if err == idb.ErrorNotInitialized {
+		// Shouldn't end up in the migration if this were the case.
+		round = 0
+	} else if err != nil {
 		db.log.WithError(err).Errorf("m6: problem caching max round: %v", err)
 		return err
 	}
@@ -833,7 +850,7 @@ func warnUser(db *IndexerDb, maxRound uint32) error {
 	}
 
 	// This many accounts need about 4GB of memory.
-	threshold := 10000000/3*4
+	threshold := 10000000 / 3 * 4
 	if count > uint64(threshold) {
 		db.log.Print("The current migration (m7) is likely to use more than 4GB of RAM.")
 
@@ -1119,7 +1136,7 @@ func m7RewardsAndDatesPart2(db *IndexerDb, state *MigrationState) error {
 		if err != nil {
 			return err
 		}
-		// Finally, read all accounts from most recent to oldest, update rewards and create/close dates,
+		// Finally, read all transactions from most recent to oldest, update rewards and create/close dates,
 		// and write this account data to the database. To save memory, this function removes account's
 		// data as soon as we reach the transaction that created this account at which point older
 		// transactions cannot update its state. It writes account data to the database in batches.
@@ -1194,8 +1211,7 @@ func (mtxid *txidFiuxpMigrationContext) asyncTxidFixup() (err error) {
 	db := mtxid.db
 	state := mtxid.state
 	db.log.Println("txid fixup migration starting")
-	prevRound := state.NextRound - 1
-	txns := db.YieldTxns(context.Background(), prevRound)
+	txns := db.YieldTxns(context.Background(), uint64(state.NextRound))
 	batch := make([]idb.TxnRow, 15000)
 	txInBatch := 0
 	roundsInBatch := 0
@@ -1822,9 +1838,9 @@ func m10SpecialAccountCleanup(db *IndexerDb, state *MigrationState) error {
 		initstmt.Exec(address[:])
 	}
 
-	upsertMigrationState(tx, state, true)
+	upsertMigrationStateTx(tx, state, true)
 	if err != nil {
-		return fmt.Errorf("m10 metstate upsert error: %v", err)
+		return fmt.Errorf("m10 metastate upsert error: %v", err)
 	}
 
 	err = tx.Commit()
@@ -1841,9 +1857,9 @@ func m10SpecialAccountCleanup(db *IndexerDb, state *MigrationState) error {
 // $2 = the holder account
 // $3 = assetID
 // $4 = round limit
-var freezeTransactionsQuery = `select count(*) from txn_participation p JOIN txn t ON t.round = p.round AND p.intra = t.intra 
-WHERE p.addr = $1 
-AND (t.txn -> 'txn' ->> 'fadd' = $2) 
+var freezeTransactionsQuery = `select count(*) from txn_participation p JOIN txn t ON t.round = p.round AND p.intra = t.intra
+WHERE p.addr = $1
+AND (t.txn -> 'txn' ->> 'fadd' = $2)
 AND t.asset = $3
 AND p.round > $4
 LIMIT 1`
@@ -1874,7 +1890,6 @@ func updateFrozenState(db *IndexerDb, assetID uint64, closedAt *uint64, creator,
 		return nil
 	}
 
-
 	// If there were no freeze transactions, re-initialize the frozen value.
 	frozen := !bytes.Equal(creator[:], holder[:])
 	db.db.Exec(`UPDATE account_asset SET frozen = $1 WHERE assetid = $2 and addr = $3`, frozen, assetID, holder[:])
@@ -1936,13 +1951,257 @@ func m11AssetHoldingFrozen(db *IndexerDb, state *MigrationState) error {
 		}
 	}
 
-	tx, err := db.db.BeginTx(context.Background(), &serializable)
-	tx.Rollback()
-	upsertMigrationState(tx, state, true)
+	upsertMigrationState(db, state, true)
+	if err != nil {
+		return fmt.Errorf("m11 metastate upsert error: %v", err)
+	}
+
+	return nil
+}
+
+// Reusable update batch function. Provide a query and an array of argument arrays to pass  to that query.
+func updateBatch(db *IndexerDb, updateQuery string, data [][]interface{}) error {
+	db.accountingLock.Lock()
+	defer db.accountingLock.Unlock()
+
+	tx, err := db.db.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.Rollback() // ignored if .Commit() first
+
+	update, err := tx.Prepare(updateQuery)
+	if err != nil {
+		return fmt.Errorf("error preparing update query: %v", err)
+	}
+	defer update.Close()
+
+	for _, txpr := range data {
+		_, err = update.Exec(txpr...)
+		if err != nil {
+			return fmt.Errorf("problem updating row (%v): %v", txpr, err)
+		}
+	}
+
+	return tx.Commit()
+}
+
+// FixFreezeLookupMigration is a migration to add txn_participation entries for freeze address in freeze transactions.
+func FixFreezeLookupMigration(db *IndexerDb, state *MigrationState) error {
+	// Technically with this query no transactions are needed, and the accounting state doesn't need to be locked.
+	updateQuery := "INSERT INTO txn_participation (addr, round, intra) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING"
+	query := fmt.Sprintf("select decode(txn.txn->'txn'->>'fadd','base64'),round,intra from txn where typeenum = %d AND txn.txn->'txn'->'snd' != txn.txn->'txn'->'fadd'", idb.TypeEnumAssetFreeze)
+	rows, err := db.db.Query(query)
+	if err != nil {
+		return fmt.Errorf("unable to query transactions: %v", err)
+	}
+	defer rows.Close()
+
+	txprows := make([][]interface{}, 0)
+
+	// Loop through all transactions and compute account data.
+	db.log.Print("loop through all freeze transactions")
+	for rows.Next() {
+		var addr []byte
+		var round, intra uint64
+		err = rows.Scan(&addr, &round, &intra)
+		if err != nil {
+			return fmt.Errorf("error scanning row: %v", err)
+		}
+
+		txprows = append(txprows, []interface{}{addr, round, intra})
+
+		if len(txprows) > 5000 {
+			err = updateBatch(db, updateQuery, txprows)
+			if err != nil {
+				return fmt.Errorf("updating batch: %v", err)
+			}
+			txprows = txprows[:0]
+		}
+	}
+
+	if rows.Err() != nil {
+		return fmt.Errorf("error while processing freeze transactions: %v", rows.Err())
+	}
+
+	// Commit any leftovers
+	if len(txprows) > 0 {
+		err = updateBatch(db, updateQuery, txprows)
+		if err != nil {
+			return fmt.Errorf("updating batch: %v", err)
+		}
+	}
+
+	// Update migration state
+	return upsertMigrationState(db, state, true)
+}
+
+type account struct {
+	address  sdk_types.Address
+	closedAt uint64 // the round when the account was last closed
+}
+
+func getAccounts(db *sql.DB) ([]account, error) {
+	query := "SELECT addr, closed_at FROM account WHERE closed_at IS NOT NULL AND deleted = false " +
+		"AND account_data IS NOT NULL"
+	rows, err := db.Query(query)
+	if err != nil {
+		return []account{}, err
+	}
+	defer rows.Close()
+
+	var res []account
+
+	for rows.Next() {
+		var addrBytes []byte
+		var closedAt sql.NullInt64
+
+		err = rows.Scan(&addrBytes, &closedAt)
+		if err != nil {
+			return []account{}, err
+		}
+
+		var addr sdk_types.Address
+		copy(addr[:], addrBytes)
+		res = append(res, account{addr, uint64(closedAt.Int64)})
+	}
+	if err := rows.Err(); err != nil {
+		return []account{}, err
+	}
+
+	return res, nil
+}
+
+func fixAuthAddr(db *IndexerDb, account account) error {
+	db.accountingLock.Lock()
+	defer db.accountingLock.Unlock()
+
+	f := func(ctx context.Context, tx *sql.Tx) error {
+		defer tx.Rollback()
+
+		rowsCh := make(chan idb.TxnRow)
+
+		// This will not work properly if the account was closed and reopened in the same round
+		// but that's unlikely to happen.
+		trueValue := true
+		tf := idb.TransactionFilter{
+			Address:     account.address[:],
+			AddressRole: idb.AddressRoleSender,
+			RekeyTo:     &trueValue,
+			MinRound:    account.closedAt,
+			Limit:       1,
+		}
+		go func() {
+			db.yieldTxns(ctx, tx, tf, rowsCh)
+			close(rowsCh)
+		}()
+
+		found := false
+		for txnRow := range rowsCh {
+			if txnRow.Error != nil {
+				return txnRow.Error
+			}
+			found = true
+		}
+
+		if found {
+			return nil
+		}
+
+		// No results. Delete the key.
+		db.log.Printf("clearing auth addr for account %s", account.address.String())
+
+		query := "UPDATE account SET account_data = account_data - 'spend' WHERE addr = $1"
+		_, err := tx.Exec(query, account.address[:])
+		if err != nil {
+			return err
+		}
+
+		return tx.Commit()
+	}
+
+	return db.txWithRetry(context.Background(), serializable, f)
+}
+
+func fixKeyreg(db *IndexerDb, account account) error {
+	db.accountingLock.Lock()
+	defer db.accountingLock.Unlock()
+
+	f := func(ctx context.Context, tx *sql.Tx) error {
+		defer tx.Rollback()
+
+		rowsCh := make(chan idb.TxnRow)
+
+		tf := idb.TransactionFilter{
+			Address:     account.address[:],
+			AddressRole: idb.AddressRoleSender,
+			MinRound:    account.closedAt,
+			TypeEnum:    idb.TypeEnumKeyreg,
+			Limit:       1,
+		}
+		go func() {
+			db.yieldTxns(ctx, tx, tf, rowsCh)
+			close(rowsCh)
+		}()
+
+		found := false
+		for txnRow := range rowsCh {
+			if txnRow.Error != nil {
+				return txnRow.Error
+			}
+			found = true
+		}
+
+		if found {
+			return nil
+		}
+
+		// No results. Delete keyreg fields.
+		db.log.Printf("clearing keyreg fields for account %s", account.address.String())
+
+		query := "UPDATE account SET account_data = account_data - 'vote' - 'sel' - 'onl' - " +
+			"'voteFst' - 'voteLst' - 'voteKD' WHERE addr = $1"
+		_, err := tx.Exec(query, account.address[:])
+		if err != nil {
+			return err
+		}
+
+		return tx.Commit()
+	}
+
+	return db.txWithRetry(context.Background(), serializable, f)
+}
+
+// ClearAccountDataMigration clears account data for accounts that have been closed.
+func ClearAccountDataMigration(db *IndexerDb, state *MigrationState) error {
+	// Clear account_data column for deleted accounts.
+	query := "UPDATE account SET account_data = NULL WHERE deleted = true;"
+	if _, err := db.db.Exec(query); err != nil {
+		return fmt.Errorf("error clearing deleted accounts: %v", err)
+	}
+
+	// Clear account data for some reopened accounts.
+	accounts, err := getAccounts(db.db)
+	if err != nil {
+		return fmt.Errorf("error getting accounts: %v", err)
+	}
+	db.log.Printf("checking %d accounts that are reopened and have account data", len(accounts))
+
+	for _, account := range accounts {
+		if err := fixAuthAddr(db, account); err != nil {
+			return fmt.Errorf("error clearing auth addr: %v", err)
+		}
+		if err := fixKeyreg(db, account); err != nil {
+			return fmt.Errorf("error clearing keyreg fields: %v", err)
+		}
+	}
+
+	state.NextMigration++
+	migrationStateJSON := idb.JSONOneLine(state)
+	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
-		return fmt.Errorf("m11 metstate upsert error: %v", err)
+		return fmt.Errorf("metastate upsert error: %v", err)
 	}
-	tx.Commit()
 
 	return nil
 }
diff --git a/idb/postgres/reset.sql b/idb/postgres/reset.sql
new file mode 100644
index 0000000..46c61bd
--- /dev/null
+++ b/idb/postgres/reset.sql
@@ -0,0 +1,7 @@
+DROP TABLE IF EXISTS account;
+DROP TABLE IF EXISTS account_app;
+DROP TABLE IF EXISTS account_asset;
+DROP TABLE IF EXISTS app;
+DROP TABLE IF EXISTS asset;
+DROP TABLE IF EXISTS metastate;
+UPDATE txn SET extra = NULL WHERE extra IS NOT NULL;
diff --git a/idb/postgres/reset_sql.go b/idb/postgres/reset_sql.go
new file mode 100644
index 0000000..0fc6a75
--- /dev/null
+++ b/idb/postgres/reset_sql.go
@@ -0,0 +1,12 @@
+// Code generated from source reset.sql via go generate. DO NOT EDIT.
+
+package postgres
+
+const reset_sql = `DROP TABLE IF EXISTS account;
+DROP TABLE IF EXISTS account_app;
+DROP TABLE IF EXISTS account_asset;
+DROP TABLE IF EXISTS app;
+DROP TABLE IF EXISTS asset;
+DROP TABLE IF EXISTS metastate;
+UPDATE txn SET extra = NULL WHERE extra IS NOT NULL;
+`
diff --git a/idb/postgres/setup_postgres.sql b/idb/postgres/setup_postgres.sql
index 5947071..ab8e12d 100644
--- a/idb/postgres/setup_postgres.sql
+++ b/idb/postgres/setup_postgres.sql
@@ -2,11 +2,6 @@
 --
 -- TODO? replace all 'addr bytea' with 'addr_id bigint' and a mapping table? makes addrs an 8 byte int that fits in a register instead of a 32 byte string
 
-CREATE TABLE IF NOT EXISTS protocol (
-  version text PRIMARY KEY,
-  proto jsonb
-);
-
 CREATE TABLE IF NOT EXISTS block_header (
 round bigint PRIMARY KEY,
 realtime timestamp without time zone NOT NULL,
@@ -45,9 +40,6 @@ intra smallint NOT NULL
 -- For query account transactions
 CREATE UNIQUE INDEX IF NOT EXISTS txn_participation_i ON txn_participation ( addr, round DESC, intra DESC );
 
--- bookeeping for local file import
-CREATE TABLE IF NOT EXISTS imported (path text);
-
 -- expand data.basics.AccountData
 CREATE TABLE IF NOT EXISTS account (
   addr bytea primary key,
diff --git a/idb/postgres/setup_postgres_sql.go b/idb/postgres/setup_postgres_sql.go
index 8f86e28..defb905 100644
--- a/idb/postgres/setup_postgres_sql.go
+++ b/idb/postgres/setup_postgres_sql.go
@@ -6,11 +6,6 @@ const setup_postgres_sql = `-- This file is setup_postgres.sql which gets compil
 --
 -- TODO? replace all 'addr bytea' with 'addr_id bigint' and a mapping table? makes addrs an 8 byte int that fits in a register instead of a 32 byte string
 
-CREATE TABLE IF NOT EXISTS protocol (
-  version text PRIMARY KEY,
-  proto jsonb
-);
-
 CREATE TABLE IF NOT EXISTS block_header (
 round bigint PRIMARY KEY,
 realtime timestamp without time zone NOT NULL,
@@ -49,9 +44,6 @@ intra smallint NOT NULL
 -- For query account transactions
 CREATE UNIQUE INDEX IF NOT EXISTS txn_participation_i ON txn_participation ( addr, round DESC, intra DESC );
 
--- bookeeping for local file import
-CREATE TABLE IF NOT EXISTS imported (path text);
-
 -- expand data.basics.AccountData
 CREATE TABLE IF NOT EXISTS account (
   addr bytea primary key,
diff --git a/importer/helper.go b/importer/helper.go
index 906c830..9cec894 100644
--- a/importer/helper.go
+++ b/importer/helper.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/algorand/go-algorand-sdk/client/v2/algod"
 	"github.com/algorand/go-algorand-sdk/encoding/json"
 	log "github.com/sirupsen/logrus"
 
@@ -57,9 +58,6 @@ type ImportState struct {
 
 // Import is the main ImportHelper function that glues together a directory full of block files and an Importer objects.
 func (h *ImportHelper) Import(db idb.IndexerDb, args []string) {
-	err := ImportProto(db)
-	maybeFail(err, h.Log, "import proto, %v", err)
-
 	imp := NewDBImporter(db)
 	blocks := 0
 	txCount := 0
@@ -90,7 +88,26 @@ func (h *ImportHelper) Import(db idb.IndexerDb, args []string) {
 		h.Log.Infof("%d blocks in %s, %.0f/s, %d txn, %.0f/s", blocks, dt.String(), float64(time.Second)*float64(blocks)/float64(dt), txCount, float64(time.Second)*float64(txCount)/float64(dt))
 	}
 
-	accountingRounds, txnCount := updateAccounting(db, h.DefaultFrozenCache, 0, h.GenesisJSONPath, h.NumRoundsLimit, h.Log)
+	state, err := db.GetImportState()
+	var startRound uint64 = 0
+	accountingRounds := 0
+	if err == idb.ErrorNotInitialized {
+		if InitialImport(db, h.GenesisJSONPath, nil, h.Log) {
+			accountingRounds++
+		}
+	} else {
+		maybeFail(err, h.Log, "problem getting the import state")
+		startRound = uint64(state.AccountRound + 1)
+	}
+
+	filter := idb.UpdateFilter{
+		StartRound: startRound,
+	}
+	if h.NumRoundsLimit != 0 {
+		filter.RoundLimit = &h.NumRoundsLimit
+	}
+	updateRounds, txnCount := updateAccounting(db, h.DefaultFrozenCache, filter, h.Log)
+	accountingRounds += updateRounds
 
 	accountingdone := time.Now()
 	if accountingRounds > 0 {
@@ -166,11 +183,6 @@ func importFile(db idb.IndexerDb, imp Importer, fname string, l *log.Logger) (bl
 	blocks = 0
 	txCount = 0
 	var btxns int
-	imported, err := db.AlreadyImported(fname)
-	maybeFail(err, l, "%s: %v", fname, err)
-	if imported {
-		return
-	}
 	l.Infof("importing %s ...", fname)
 	if strings.HasSuffix(fname, ".tar") {
 		fin, err := os.Open(fname)
@@ -198,8 +210,6 @@ func importFile(db idb.IndexerDb, imp Importer, fname string, l *log.Logger) (bl
 		blocks++
 		txCount += btxns
 	}
-	err = db.MarkImported(fname)
-	maybeFail(err, l, "%s: %v", fname, err)
 	return
 }
 
@@ -217,38 +227,50 @@ func loadGenesis(db idb.IndexerDb, in io.Reader) (err error) {
 	return db.LoadGenesis(genesis)
 }
 
+// InitialImport imports the genesis block if needed. Returns true if the initial import occurred.
+func InitialImport(db idb.IndexerDb, genesisJSONPath string, client *algod.Client, l *log.Logger) bool {
+	_, err := db.GetImportState()
+
+	// Exit immediately or crash if we don't see ErrorNotInitialized.
+	if err != idb.ErrorNotInitialized {
+		maybeFail(err, l, "getting import state, %v", err)
+		return false
+	}
+
+	// Import genesis file from file or algod.
+	var genesisReader io.Reader
+
+	if genesisJSONPath != "" {
+		// Read file if specified.
+		l.Infof("loading genesis file %s", genesisJSONPath)
+		genesisReader, err = os.Open(genesisJSONPath)
+		maybeFail(err, l, "unable to read genesis file %s", genesisJSONPath)
+	} else if client != nil {
+		// Fallback to asking algod for genesis if file is not specified.
+		l.Infof("fetching genesis from algod")
+		genesisString, err := client.GetGenesis().Do(context.Background())
+		maybeFail(err, l, "unable to fetch genesis from algod")
+		genesisReader = strings.NewReader(genesisString)
+	} else {
+		l.Fatal("Neither genesis file path or algod client provided for initial import.")
+	}
+
+	err = loadGenesis(db, genesisReader)
+	maybeFail(err, l, "%s: could not load genesis json, %v", genesisJSONPath, err)
+	return true
+}
+
 // UpdateAccounting triggers an accounting update.
-func UpdateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, round uint64, genesisJSONPath string, l *log.Logger) (rounds, txnCount int) {
-	return updateAccounting(db, frozenCache, round, genesisJSONPath, 0, l)
+func UpdateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, filter idb.UpdateFilter, l *log.Logger) (rounds, txnCount int) {
+	return updateAccounting(db, frozenCache, filter, l)
 }
 
-func updateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, round uint64, genesisJSONPath string, numRoundsLimit int, l *log.Logger) (rounds, txnCount int) {
+func updateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, filter idb.UpdateFilter, l *log.Logger) (rounds, txnCount int) {
 	rounds = 0
 	txnCount = 0
-	state, err := db.GetImportState()
-	maybeFail(err, l, "getting import state, %v", err)
-	if state == nil {
-		state = &idb.ImportState{AccountRound: -1}
-		if genesisJSONPath != "" {
-			l.Infof("loading genesis %s", genesisJSONPath)
-			// if we're given no previous state and we're given a genesis file, import it as initial account state
-			gf, err := os.Open(genesisJSONPath)
-			maybeFail(err, l, "%s: %v", genesisJSONPath, err)
-			err = loadGenesis(db, gf)
-			maybeFail(err, l, "%s: could not load genesis json, %v", genesisJSONPath, err)
-			rounds++
-		} else {
-			l.Errorf("no import state recorded; need --genesis genesis.json file to get started")
-			os.Exit(1)
-			return
-		}
-	} else {
-		l.Infof("will start from round >%d", state.AccountRound)
-	}
-
 	lastlog := time.Now()
 	act := accounting.New(frozenCache)
-	txns := db.YieldTxns(context.Background(), state.AccountRound)
+	txns := db.YieldTxns(context.Background(), filter.StartRound)
 	currentRound := uint64(0)
 	roundsSeen := 0
 	lastRoundsSeen := roundsSeen
@@ -258,7 +280,7 @@ func updateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, round uint6
 		maybeFail(txn.Error, l, "updateAccounting txn fetch, %v", txn.Error)
 		if txn.Round != currentRound {
 			if blockPtr != nil && txnForRound > 0 {
-				err = db.CommitRoundAccounting(act.RoundUpdates, currentRound, blockPtr)
+				err := db.CommitRoundAccounting(act.RoundUpdates, currentRound, blockPtr)
 				maybeFail(err, l, "failed to commit round accounting")
 			}
 
@@ -267,14 +289,20 @@ func updateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, round uint6
 			prevRound := currentRound
 			roundsSeen++
 			currentRound = txn.Round
+
+			// Check to see if the max round has been reached after resetting txnForRound.
+			if filter.MaxRound != 0 && currentRound > filter.MaxRound {
+				break
+			}
+
 			block, _, err := db.GetBlock(context.Background(), currentRound, idb.GetBlockOptions{})
 			maybeFail(err, l, "problem fetching next round (%d)", currentRound)
 			blockPtr = &block
 			act.InitRound(block)
 
 			// Log progress
-			if (numRoundsLimit != 0) && (roundsSeen > numRoundsLimit) {
-				l.Infof("hit rounds limit %d > %d", roundsSeen, numRoundsLimit)
+			if (filter.RoundLimit != nil) && (roundsSeen > *filter.RoundLimit) {
+				l.Infof("hit rounds limit %d > %d", roundsSeen, filter.RoundLimit)
 				break
 			}
 			now := time.Now()
@@ -286,18 +314,15 @@ func updateAccounting(db idb.IndexerDb, frozenCache map[uint64]bool, round uint6
 				lastRoundsSeen = roundsSeen
 			}
 		}
-		err = act.AddTransaction(&txn)
+		err := act.AddTransaction(&txn)
 		maybeFail(err, l, "txn accounting r=%d i=%d, %v", txn.Round, txn.Intra, err)
 		txnCount++
 		txnForRound++
 	}
 
 	// Commit the final round
-	if (blockPtr != nil && txnForRound > 0) || ((round != 0) && (currentRound < round)) {
-		if currentRound < round {
-			currentRound = round
-		}
-		err = db.CommitRoundAccounting(act.RoundUpdates, currentRound, blockPtr)
+	if blockPtr != nil && txnForRound > 0 {
+		err := db.CommitRoundAccounting(act.RoundUpdates, currentRound, blockPtr)
 		maybeFail(err, l, "failed to commit round accounting")
 	}
 
diff --git a/importer/importer.go b/importer/importer.go
index 27abe63..70244fb 100644
--- a/importer/importer.go
+++ b/importer/importer.go
@@ -113,6 +113,7 @@ func (imp *dbImporter) ImportDecodedBlock(blockContainer *types.EncodedBlockCert
 		participants = participate(participants, stxn.Txn.AssetSender[:])
 		participants = participate(participants, stxn.Txn.AssetReceiver[:])
 		participants = participate(participants, stxn.Txn.AssetCloseTo[:])
+		participants = participate(participants, stxn.Txn.FreezeAccount[:])
 		err = imp.db.AddTransaction(round, intra, txtypeenum, assetid, stxnad, participants)
 		if err != nil {
 			return txCount, fmt.Errorf("error importing txn r=%d i=%d, %v", round, intra, err)
@@ -133,15 +134,3 @@ func (imp *dbImporter) ImportDecodedBlock(blockContainer *types.EncodedBlockCert
 func NewDBImporter(db idb.IndexerDb) Importer {
 	return &dbImporter{db: db}
 }
-
-// ImportProto writes compiled-in protocol information to the database
-func ImportProto(db idb.IndexerDb) (err error) {
-	types.ForeachProtocol(func(version string, proto types.ConsensusParams) (err error) {
-		err = db.SetProto(version, proto)
-		if err != nil {
-			return fmt.Errorf("db set proto %s, %v", version, err)
-		}
-		return nil
-	})
-	return nil
-}
diff --git a/misc/convertToAddress.go b/misc/convertToAddress.go
index a3e56a4..c93acba 100644
--- a/misc/convertToAddress.go
+++ b/misc/convertToAddress.go
@@ -5,7 +5,7 @@ import (
 	"flag"
 	"fmt"
 
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 )
 
 func main() {
@@ -16,7 +16,7 @@ func main() {
 	addrBytes, err := base64.StdEncoding.DecodeString(addrInput)
 	if err != nil {
 		// Failed to base64 decode, try algorand.
-		a, err := types.DecodeAddress(addrInput)
+		a, err := sdk_types.DecodeAddress(addrInput)
 		if err != nil {
 			fmt.Println(err)
 			return
@@ -24,7 +24,7 @@ func main() {
 		fmt.Println(base64.StdEncoding.EncodeToString(a[:]))
 		return
 	}
-	var addr types.Address
+	var addr sdk_types.Address
 	copy(addr[:], addrBytes)
 	fmt.Println(addr.String())
 }
diff --git a/types/consensus.go b/types/consensus.go
index 597aef8..6e96de9 100644
--- a/types/consensus.go
+++ b/types/consensus.go
@@ -49,18 +49,3 @@ func Protocol(version string) (proto ConsensusParams, err error) {
 	}
 	return
 }
-
-// ForeachProtocol executes a function against each protocol version.
-func ForeachProtocol(f func(version string, proto ConsensusParams) error) (err error) {
-	err = ensureProtos()
-	if err != nil {
-		return
-	}
-	for version, proto := range protocols {
-		err = f(version, proto)
-		if err != nil {
-			return
-		}
-	}
-	return nil
-}
diff --git a/types/types.go b/types/types.go
index 0da9eaa..c6b24db 100644
--- a/types/types.go
+++ b/types/types.go
@@ -4,14 +4,14 @@ package types
 import (
 	"time"
 
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 )
 
 type (
 	// Address alias to SDK address.
-	Address = atypes.Address // [32]byte
+	Address = sdk_types.Address // [32]byte
 	// Digest is a hash value.
-	Digest = atypes.Digest // [32]byte
+	Digest = sdk_types.Digest // [32]byte
 
 	// Seed used by sortition.
 	Seed [32]byte
@@ -230,7 +230,7 @@ type (
 	SignedTxnWithAD struct {
 		_struct struct{} `codec:",omitempty,omitemptyarray"`
 
-		atypes.SignedTxn
+		sdk_types.SignedTxn
 		ApplyData
 	}
 
@@ -283,9 +283,9 @@ type (
 	DeltaAction uint64
 
 	// Transaction alias for the SDK transaction
-	Transaction = atypes.Transaction
+	Transaction = sdk_types.Transaction
 	// AssetParams alias for the SDK asset params
-	AssetParams = atypes.AssetParams
+	AssetParams = sdk_types.AssetParams
 
 	// EncodedBlockCert is the block encoded along with its certificate.
 	EncodedBlockCert struct {
@@ -878,13 +878,13 @@ const (
 )
 
 // MergeAssetConfig merges together two asset param objects.
-func MergeAssetConfig(old, new atypes.AssetParams) (out atypes.AssetParams) {
+func MergeAssetConfig(old, new sdk_types.AssetParams) (out sdk_types.AssetParams) {
 	// if asset is new, set.
 	// if new config is empty, set empty.
 	// else, update.
-	if old == (atypes.AssetParams{}) {
+	if old == (sdk_types.AssetParams{}) {
 		out = new
-	} else if new == (atypes.AssetParams{}) {
+	} else if new == (sdk_types.AssetParams{}) {
 		out = new
 	} else {
 		out = old
diff --git a/util/util.go b/util/util.go
index 27ec8e9..7420adf 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,6 +1,10 @@
 package util
 
-import "strings"
+import (
+	"fmt"
+	"os"
+	"strings"
+)
 
 // KeysStringInt returns all of the keys in the map joined by a comma.
 func KeysStringInt(m map[string]int) string {
@@ -19,3 +23,13 @@ func KeysStringBool(m map[string]bool) string {
 	}
 	return strings.Join(keys, ", ")
 }
+
+// MaybeFail exits if there was an error.
+func MaybeFail(err error, errfmt string, params ...interface{}) {
+	if err == nil {
+		return
+	}
+	fmt.Fprintf(os.Stderr, errfmt, params...)
+	fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
+	os.Exit(1)
+}


Test patch:
diff --git a/accounting/accounting_test.go b/accounting/accounting_test.go
index 45ca571..7305378 100644
--- a/accounting/accounting_test.go
+++ b/accounting/accounting_test.go
@@ -3,7 +3,7 @@ package accounting
 import (
 	"testing"
 
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/algorand/indexer/idb"
@@ -22,19 +22,19 @@ func assertUpdates(t *testing.T, update *idb.AlgoUpdate, closed bool, balance, r
 	assert.Equal(t, rewards, update.Rewards)
 }
 
-func getSenderAmounts(txn *types.SignedTxnWithAD) (balance, rewards int64) {
+func getSenderAmounts(txn *sdk_types.SignedTxnWithAD) (balance, rewards int64) {
 	balance = -int64(txn.Txn.Amount) + -int64(txn.Txn.Fee) + -int64(txn.ClosingAmount) + int64(txn.SenderRewards)
 	rewards = int64(txn.SenderRewards)
 	return
 }
 
-func getReceiverAmounts(txn *types.SignedTxnWithAD) (balance, rewards int64) {
+func getReceiverAmounts(txn *sdk_types.SignedTxnWithAD) (balance, rewards int64) {
 	balance = int64(txn.Txn.Amount) + int64(txn.ReceiverRewards)
 	rewards = int64(txn.ReceiverRewards)
 	return
 }
 
-func getCloseAmounts(txn *types.SignedTxnWithAD) (balance, rewards int64) {
+func getCloseAmounts(txn *sdk_types.SignedTxnWithAD) (balance, rewards int64) {
 	balance = int64(txn.ClosingAmount) + int64(txn.CloseRewards)
 	rewards = int64(txn.CloseRewards)
 	return
@@ -111,8 +111,8 @@ func TestAssetCloseReopenPay(t *testing.T) {
 	assetid := uint64(22222)
 	amt := uint64(10000)
 	_, closeMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountB, test.AccountB)
-	_, optinMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, types.ZeroAddress)
-	_, payMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountB, test.AccountA, types.ZeroAddress)
+	_, optinMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress)
+	_, payMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountB, test.AccountA, sdk_types.ZeroAddress)
 
 	state := GetAccounting()
 	state.AddTransaction(closeMain)
diff --git a/accounting/rewind_test.go b/accounting/rewind_test.go
index 01c9c03..1cacfb5 100644
--- a/accounting/rewind_test.go
+++ b/accounting/rewind_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 
@@ -15,7 +15,7 @@ import (
 )
 
 func TestBasic(t *testing.T) {
-	var a types.Address
+	var a sdk_types.Address
 	a[0] = 'a'
 
 	account := models.Account{
@@ -25,11 +25,11 @@ func TestBasic(t *testing.T) {
 		Round:                       8,
 	}
 
-	txnBytes := msgpack.Encode(types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
-				Type: types.PaymentTx,
-				PaymentTxnFields: types.PaymentTxnFields{
+	txnBytes := msgpack.Encode(sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
+				Type: sdk_types.PaymentTx,
+				PaymentTxnFields: sdk_types.PaymentTxnFields{
 					Receiver: a,
 					Amount:   2,
 				},
@@ -58,7 +58,7 @@ func TestBasic(t *testing.T) {
 
 // Test that when idb.Transactions() returns stale data the first time, we return an error.
 func TestStaleTransactions1(t *testing.T) {
-	var a types.Address
+	var a sdk_types.Address
 	a[0] = 'a'
 
 	account := models.Account{
@@ -78,7 +78,7 @@ func TestStaleTransactions1(t *testing.T) {
 
 // Test that when idb.Transactions() returns stale data the second time, we return an error.
 func TestStaleTransactions2(t *testing.T) {
-	var a types.Address
+	var a sdk_types.Address
 	a[0] = 'a'
 
 	account := models.Account{
@@ -88,10 +88,10 @@ func TestStaleTransactions2(t *testing.T) {
 		Round:                       8,
 	}
 
-	txnBytes := msgpack.Encode(types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
-				Type: types.PaymentTx,
+	txnBytes := msgpack.Encode(sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
+				Type: sdk_types.PaymentTx,
 			},
 		},
 	})
diff --git a/api/handlers_test.go b/api/handlers_test.go
index dbc3f06..e5ec0e8 100644
--- a/api/handlers_test.go
+++ b/api/handlers_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"strings"
 	"testing"
 	"time"
 
@@ -114,12 +115,12 @@ func TestTransactionParamToTransactionFilter(t *testing.T) {
 				AssetID:           4,
 				BeforeTime:        time.Date(2021, 1, 1, 1, 0, 0, 0, time.FixedZone("UTC", 0)),
 				AfterTime:         time.Date(2022, 2, 2, 2, 0, 0, 0, time.FixedZone("UTC", 0)),
-				AlgosGT:           0,
-				AlgosLT:           0,
-				AssetAmountGT:     5,
-				AssetAmountLT:     6,
-				EffectiveAmountGt: 0,
-				EffectiveAmountLt: 0,
+				AlgosGT:           nil,
+				AlgosLT:           nil,
+				AssetAmountGT:     uint64Ptr(5),
+				AssetAmountLT:     uint64Ptr(6),
+				EffectiveAmountGT: nil,
+				EffectiveAmountLT: nil,
 				Address:           []byte{197, 204, 27, 84, 42, 255, 184, 163, 97, 247, 241, 26, 128, 13, 87, 125, 150, 22, 131, 63, 100, 238, 60, 209, 121, 177, 156, 23, 106, 222, 64, 35},
 				AddressRole:       9,
 				Offset:            nil,
@@ -196,7 +197,7 @@ func TestTransactionParamToTransactionFilter(t *testing.T) {
 		{
 			name:          "Currency to Algos when no asset-id",
 			params:        generated.SearchForTransactionsParams{CurrencyGreaterThan: uint64Ptr(10), CurrencyLessThan: uint64Ptr(20)},
-			filter:        idb.TransactionFilter{AlgosGT: 10, AlgosLT: 20, Limit: defaultTransactionsLimit},
+			filter:        idb.TransactionFilter{AlgosGT: uint64Ptr(10), AlgosLT: uint64Ptr(20), Limit: defaultTransactionsLimit},
 			errorContains: nil,
 		},
 		{
@@ -425,7 +426,6 @@ func TestFetchTransactions(t *testing.T) {
 			// Setup the mocked responses
 
 			mockIndexer := &mocks.IndexerDb{}
-			indexerDb = mockIndexer
 			si := ServerImplementation{
 				EnableAddressSearchRoundRewind: true,
 				db:                             mockIndexer,
@@ -483,3 +483,21 @@ func TestFetchTransactions(t *testing.T) {
 		})
 	}
 }
+
+func TestFetchAccountsRewindRoundTooLarge(t *testing.T) {
+	ch := make(chan idb.AccountRow)
+	close(ch)
+	var outCh <-chan idb.AccountRow = ch
+
+	db := &mocks.IndexerDb{}
+	db.On("GetAccounts", mock.Anything, mock.Anything).Return(outCh, uint64(7)).Once()
+
+	si := ServerImplementation{
+		EnableAddressSearchRoundRewind: true,
+		db:                             db,
+	}
+	atRound := uint64(8)
+	_, _, err := si.fetchAccounts(context.Background(), idb.AccountQueryOptions{}, &atRound)
+	assert.Error(t, err)
+	assert.True(t, strings.HasPrefix(err.Error(), errRewindingAccount), err.Error())
+}
diff --git a/api/middlewares/migration_middleware_test.go b/api/middlewares/migration_middleware_test.go
index 8fc017a..88d03ec 100644
--- a/api/middlewares/migration_middleware_test.go
+++ b/api/middlewares/migration_middleware_test.go
@@ -14,7 +14,6 @@ import (
 
 var errSuccess = errors.New("unexpected success")
 var e = echo.New()
-var testAPIHeader = "API-Header-Whatever"
 
 // success is the "next" handler, it is only called when a request is allowed to continue
 func success(ctx echo.Context) error {
diff --git a/cmd/block-generator/main_test.go b/cmd/block-generator/main_test.go
new file mode 100644
index 0000000..17330e1
--- /dev/null
+++ b/cmd/block-generator/main_test.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestParseRound(t *testing.T) {
+	var testcases = []struct {
+		name          string
+		url           string
+		expectedRound uint64
+	}{
+		{
+			name:          "invalid prefix",
+			url:           fmt.Sprintf("/v2/wrong/prefix/1"),
+			expectedRound: 0,
+		},
+		{
+			name:          "normal one digit",
+			url:           fmt.Sprintf("%s1", blockQueryPrefix),
+			expectedRound: 1,
+		},
+		{
+			name:          "normal long number",
+			url:           fmt.Sprintf("%s12345678", blockQueryPrefix),
+			expectedRound: 12345678,
+		},
+		{
+			name:          "with query parameters",
+			url:           fmt.Sprintf("%s1234?pretty", blockQueryPrefix),
+			expectedRound: 1234,
+		},
+	}
+
+	for _, testcase := range testcases {
+		t.Run(testcase.name, func(t *testing.T) {
+			round, err := parseRound(testcase.url)
+			if err != nil && testcase.expectedRound != 0 {
+				assert.Fail(t, fmt.Sprintf("Unexpected error parsing '%s', expected round '%d' received error: %v", testcase.url, testcase.expectedRound, err))
+			}
+			assert.Equal(t, testcase.expectedRound, round)
+		})
+	}
+}
diff --git a/cmd/block-generator/test_config.yml b/cmd/block-generator/test_config.yml
new file mode 100644
index 0000000..6d411e9
--- /dev/null
+++ b/cmd/block-generator/test_config.yml
@@ -0,0 +1,23 @@
+name: Test Config
+# genesis accounts
+genesis_accounts: 10
+genesis_account_balance: 40000000000000
+
+tx_per_block: 10
+
+# every 100 payment transactions, create an account
+tx_pay_fraction: 0.5
+tx_asset_fraction: 0.5
+
+# transaction distribution
+pay_acct_create_fraction: 0.5
+pay_xfer_fraction: 0.5
+
+# asset distribution
+asset_create_fraction: 0.25
+asset_optin_fraction: 0.25
+asset_close_fraction: 0.25
+asset_xfer_fraction: 0.25
+asset_destroy_fraction: 0.0
+
+
diff --git a/cmd/e2equeries/main.go b/cmd/e2equeries/main.go
index 8797d76..581b4ee 100644
--- a/cmd/e2equeries/main.go
+++ b/cmd/e2equeries/main.go
@@ -2,18 +2,18 @@ package main
 
 import (
 	"context"
-	"encoding/base64"
 	"flag"
 	"fmt"
 	"os"
 	"time"
 
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/idb"
 	_ "github.com/algorand/indexer/idb/postgres"
 	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util"
 	testutil "github.com/algorand/indexer/util/test"
 )
 
@@ -23,18 +23,10 @@ var (
 	truev = true
 )
 
-var maybeFail = testutil.MaybeFail
+var maybeFail = util.MaybeFail
 var printAccountQuery = testutil.PrintAccountQuery
 var printTxnQuery = testutil.PrintTxnQuery
 
-func b64(x string) []byte {
-	v, err := base64.StdEncoding.DecodeString(x)
-	if err != nil {
-		panic(err)
-	}
-	return v
-}
-
 func main() {
 	start := time.Now()
 	flag.BoolVar(&quiet, "q", false, "")
@@ -42,14 +34,14 @@ func main() {
 	flag.Parse()
 	testutil.SetQuiet(quiet)
 
-	db, err := idb.IndexerDbByName("postgres", pgdb, &idb.IndexerDbOptions{ReadOnly: true}, nil)
+	db, err := idb.IndexerDbByName("postgres", pgdb, idb.IndexerDbOptions{ReadOnly: true}, nil)
 	maybeFail(err, "open postgres, %v", err)
 
 	rekeyTxnQuery := idb.TransactionFilter{RekeyTo: &truev, Limit: 1}
 	printTxnQuery(db, rekeyTxnQuery)
 
 	rowchan, _ := db.Transactions(context.Background(), rekeyTxnQuery)
-	var rekeyTo atypes.Address
+	var rekeyTo sdk_types.Address
 	for txnrow := range rowchan {
 		maybeFail(txnrow.Error, "err rekey txn %v\n", txnrow.Error)
 		var stxn types.SignedTxnWithAD
diff --git a/cmd/idbtest/idbtest.go b/cmd/idbtest/idbtest.go
index 4b33a36..e7267f0 100644
--- a/cmd/idbtest/idbtest.go
+++ b/cmd/idbtest/idbtest.go
@@ -10,13 +10,13 @@ import (
 	"time"
 
 	ajson "github.com/algorand/go-algorand-sdk/encoding/json"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/accounting"
 	models "github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/idb"
 	_ "github.com/algorand/indexer/idb/postgres"
-	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util"
 	testutil "github.com/algorand/indexer/util/test"
 )
 
@@ -31,7 +31,7 @@ var (
 
 var exitValue = 0
 
-var maybeFail = testutil.MaybeFail
+var maybeFail = util.MaybeFail
 var printAssetQuery func(db idb.IndexerDb, q idb.AssetsQuery) = testutil.PrintAssetQuery
 var printAccountQuery = testutil.PrintAccountQuery
 var printTxnQuery = testutil.PrintTxnQuery
@@ -43,7 +43,7 @@ func doAssetQueryTests(db idb.IndexerDb) {
 	printAssetQuery(db, idb.AssetsQuery{Unit: "USDt", Limit: 2})
 	printAssetQuery(db, idb.AssetsQuery{AssetID: 312769, Limit: 1})
 	printAssetQuery(db, idb.AssetsQuery{AssetIDGreaterThan: 312769, Query: "us", Limit: 2})
-	tcreator, err := atypes.DecodeAddress("XIU7HGGAJ3QOTATPDSIIHPFVKMICXKHMOR2FJKHTVLII4FAOA3CYZQDLG4")
+	tcreator, err := sdk_types.DecodeAddress("XIU7HGGAJ3QOTATPDSIIHPFVKMICXKHMOR2FJKHTVLII4FAOA3CYZQDLG4")
 	maybeFail(err, "addr decode, %v\n", err)
 	printAssetQuery(db, idb.AssetsQuery{Creator: tcreator[:], Limit: 1})
 }
@@ -105,19 +105,6 @@ func testTxnPaging(db idb.IndexerDb, q idb.TransactionFilter) {
 	}
 }
 
-func printAssetBalanceQuery(db idb.IndexerDb, assetID uint64) {
-	rows, _ := db.AssetBalances(context.Background(), idb.AssetBalanceQuery{AssetID: assetID})
-	count := 0
-	for row := range rows {
-		maybeFail(row.Error, "err %v\n", row.Error)
-		var addr types.Address
-		copy(addr[:], row.Address)
-		fmt.Printf("%s %d %12d %t\n", addr.String(), row.AssetID, row.Amount, row.Frozen)
-		count++
-	}
-	fmt.Printf("%d asset balances\n", count)
-}
-
 func getAccount(db idb.IndexerDb, addr []byte) (account models.Account, err error) {
 	accountchan, _ :=
 		db.GetAccounts(context.Background(), idb.AccountQueryOptions{EqualToAddress: addr})
@@ -139,11 +126,11 @@ func main() {
 	flag.Parse()
 	testutil.SetQuiet(quiet)
 
-	db, err := idb.IndexerDbByName("postgres", pgdb, nil, nil)
+	db, err := idb.IndexerDbByName("postgres", pgdb, idb.IndexerDbOptions{}, nil)
 	maybeFail(err, "open postgres, %v", err)
 
 	if accounttest {
-		printAccountQuery(db, idb.AccountQueryOptions{IncludeAssetHoldings: true, IncludeAssetParams: true, AlgosGreaterThan: 10000000000, Limit: 20})
+		printAccountQuery(db, idb.AccountQueryOptions{IncludeAssetHoldings: true, IncludeAssetParams: true, AlgosGreaterThan: uint64Ptr(10000000000), Limit: 20})
 		printAccountQuery(db, idb.AccountQueryOptions{HasAssetID: 312769, Limit: 19})
 	}
 	if assettest {
@@ -152,7 +139,7 @@ func main() {
 
 	if false {
 		// account rewind debug
-		xa, _ := atypes.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
+		xa, _ := sdk_types.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
 		account, err := getAccount(db, xa[:])
 		fmt.Printf("account %s\n", string(ajson.Encode(account)))
 		maybeFail(err, "addr lookup, %v", err)
@@ -170,7 +157,7 @@ func main() {
 
 	if txntest {
 		// txn query tests
-		xa, _ := atypes.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
+		xa, _ := sdk_types.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
 		printTxnQuery(db, idb.TransactionFilter{Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{MinRound: 5000000, Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{MaxRound: 100000, Limit: 2})
@@ -180,14 +167,14 @@ func main() {
 		printTxnQuery(db, idb.TransactionFilter{Offset: &offset, Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{SigType: "lsig", Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{NotePrefix: []byte("a"), Limit: 2})
-		printTxnQuery(db, idb.TransactionFilter{AlgosGT: 10000000, Limit: 2})
-		printTxnQuery(db, idb.TransactionFilter{EffectiveAmountGt: 10000000, Limit: 2})
-		printTxnQuery(db, idb.TransactionFilter{EffectiveAmountLt: 1000000, Limit: 2})
+		printTxnQuery(db, idb.TransactionFilter{AlgosGT: uint64Ptr(10000000), Limit: 2})
+		printTxnQuery(db, idb.TransactionFilter{EffectiveAmountGT: uint64Ptr(10000000), Limit: 2})
+		printTxnQuery(db, idb.TransactionFilter{EffectiveAmountLT: uint64Ptr(1000000), Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{Address: xa[:], Limit: 6})
 		printTxnQuery(db, idb.TransactionFilter{Address: xa[:], AddressRole: idb.AddressRoleSender, Limit: 2})
 		printTxnQuery(db, idb.TransactionFilter{Address: xa[:], AddressRole: idb.AddressRoleReceiver, Limit: 2})
-		printTxnQuery(db, idb.TransactionFilter{AssetAmountGT: 99, Limit: 2})
-		printTxnQuery(db, idb.TransactionFilter{AssetAmountLT: 100, Limit: 2})
+		printTxnQuery(db, idb.TransactionFilter{AssetAmountGT: uint64Ptr(99), Limit: 2})
+		printTxnQuery(db, idb.TransactionFilter{AssetAmountLT: uint64Ptr(100), Limit: 2})
 	}
 
 	//printTxnQuery(db, idb.TransactionFilter{AssetID: 312769, Limit: 30})
@@ -195,10 +182,10 @@ func main() {
 	//printAssetBalanceQuery(db, 312769)
 
 	if pagingtest {
-		xa, _ := atypes.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
+		xa, _ := sdk_types.DecodeAddress("QRP4AJLQXHJ42VJ5PSGAH53IVVACYCI6ZDRJMF4JPRFY5VKSYKFWKKMFVU")
 		testTxnPaging(db, idb.TransactionFilter{Address: xa[:]})
 		testTxnPaging(db, idb.TransactionFilter{TypeEnum: 2})
-		testTxnPaging(db, idb.TransactionFilter{AlgosGT: 1})
+		testTxnPaging(db, idb.TransactionFilter{AlgosGT: uint64Ptr(1)})
 	}
 
 	dt := time.Now().Sub(start)
@@ -210,3 +197,7 @@ func main() {
 	}
 	os.Exit(exitValue)
 }
+
+func uint64Ptr(x uint64) *uint64 {
+	return &x
+}
diff --git a/idb/postgres/postgres_integration_common_test.go b/idb/postgres/postgres_integration_common_test.go
new file mode 100644
index 0000000..b319e82
--- /dev/null
+++ b/idb/postgres/postgres_integration_common_test.go
@@ -0,0 +1,89 @@
+package postgres
+
+import (
+	"database/sql"
+	"fmt"
+	"testing"
+
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
+	"github.com/orlangure/gnomock"
+	"github.com/orlangure/gnomock/preset/postgres"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/algorand/indexer/accounting"
+	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/importer"
+	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util/test"
+)
+
+// setupPostgres starts a gnomock postgres DB then returns the connection string and a shutdown function.
+func setupPostgres(t *testing.T) (*sql.DB, string, func()) {
+	p := postgres.Preset(
+		postgres.WithVersion("12.5"),
+		postgres.WithUser("gnomock", "gnomick"),
+		postgres.WithDatabase("mydb"),
+	)
+	container, err := gnomock.Start(p)
+	assert.NoError(t, err, "Error starting gnomock")
+
+	shutdownFunc := func() {
+		err = gnomock.Stop(container)
+		assert.NoError(t, err, "Error stoping gnomock")
+	}
+
+	connStr := fmt.Sprintf(
+		"host=%s port=%d user=%s password=%s  dbname=%s sslmode=disable",
+		container.Host, container.DefaultPort(),
+		"gnomock", "gnomick", "mydb",
+	)
+
+	db, err := sql.Open("postgres", connStr)
+	assert.NoError(t, err, "Error opening pg connection")
+	return db, connStr, shutdownFunc
+}
+
+func setupIdb(t *testing.T) (*IndexerDb /*db*/, func() /*shutdownFunc*/) {
+	_, connStr, shutdownFunc := setupPostgres(t)
+
+	idb, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
+	assert.NoError(t, err)
+
+	return idb, shutdownFunc
+}
+
+func importTxns(t *testing.T, db *IndexerDb, round uint64, txns ...*sdk_types.SignedTxnWithAD) {
+	block := test.MakeBlockForTxns(round, txns...)
+
+	_, err := importer.NewDBImporter(db).ImportDecodedBlock(&block)
+	assert.NoError(t, err)
+}
+
+func accountTxns(t *testing.T, db *IndexerDb, round uint64, txns ...*idb.TxnRow) {
+	cache, err := db.GetDefaultFrozen()
+	assert.NoError(t, err)
+
+	state := accounting.New(cache)
+	err = state.InitRoundParts(round, test.FeeAddr, test.RewardAddr, 0)
+	assert.NoError(t, err)
+
+	for _, txn := range txns {
+		err := state.AddTransaction(txn)
+		assert.NoError(t, err)
+	}
+
+	err = db.CommitRoundAccounting(state.RoundUpdates, round, &types.Block{})
+	assert.NoError(t, err)
+}
+
+// Helper to execute a query returning an integer, for example COUNT(*). Returns -1 on an error.
+func queryInt(db *sql.DB, queryString string, args ...interface{}) int {
+	row := db.QueryRow(queryString, args...)
+
+	var count int
+	err := row.Scan(&count)
+	if err != nil {
+		return -1
+	}
+	return count
+}
diff --git a/idb/postgres/postgres_integration_test.go b/idb/postgres/postgres_integration_test.go
index 2f5f3b0..ef69101 100644
--- a/idb/postgres/postgres_integration_test.go
+++ b/idb/postgres/postgres_integration_test.go
@@ -3,23 +3,22 @@ package postgres
 import (
 	"context"
 	"database/sql"
-	"fmt"
 	"sync"
 	"testing"
 
 	_ "github.com/lib/pq"
-	"github.com/orlangure/gnomock"
-	"github.com/orlangure/gnomock/preset/postgres"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/algorand/go-algorand-sdk/crypto"
 	"github.com/algorand/go-algorand-sdk/encoding/json"
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/accounting"
 	"github.com/algorand/indexer/idb"
-	itypes "github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/importer"
+	"github.com/algorand/indexer/types"
 	"github.com/algorand/indexer/util/test"
 )
 
@@ -30,34 +29,6 @@ func getAccounting(round uint64, cache map[uint64]bool) *accounting.State {
 	return accountingState
 }
 
-// setupPostgres starts a gnomock postgres DB then returns the connection string and a shutdown function.
-func setupPostgres(t *testing.T) (*sql.DB, string, func()) {
-	p := postgres.Preset(
-		postgres.WithVersion("12.5"),
-		postgres.WithUser("gnomock", "gnomick"),
-		postgres.WithDatabase("mydb"),
-		// 'IndexerDbByName' does this if necessary, so not needed here.
-		//postgres.WithQueriesFile("setup_postgres.sql"),
-	)
-	container, err := gnomock.Start(p)
-	assert.NoError(t, err, "Error starting gnomock")
-
-	shutdownFunc := func() {
-		err = gnomock.Stop(container)
-		assert.NoError(t, err, "Error stoping gnomock")
-	}
-
-	connStr := fmt.Sprintf(
-		"host=%s port=%d user=%s password=%s  dbname=%s sslmode=disable",
-		container.Host, container.DefaultPort(),
-		"gnomock", "gnomick", "mydb",
-	)
-
-	db, err := sql.Open("postgres", connStr)
-	assert.NoError(t, err, "Error opening pg connection")
-	return db, connStr, shutdownFunc
-}
-
 // TestMaxRoundOnUninitializedDB makes sure we return 0 when getting the max round on a new DB.
 func TestMaxRoundOnUninitializedDB(t *testing.T) {
 	_, connStr, shutdownFunc := setupPostgres(t)
@@ -66,20 +37,20 @@ func TestMaxRoundOnUninitializedDB(t *testing.T) {
 	///////////
 	// Given // A database that has not yet imported the genesis accounts.
 	///////////
-	db, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	db, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	//////////
 	// When // We request the max round.
 	//////////
-	roundA, err := db.GetMaxRoundAccounted()
-	assert.NoError(t, err)
-	roundL, err := db.GetMaxRoundLoaded()
-	assert.NoError(t, err)
+	roundA, errA := db.GetMaxRoundAccounted()
+	roundL, errL := db.GetMaxRoundLoaded()
 
 	//////////
-	// Then // There should be no error and we return that there are zero rounds.
+	// Then // The error message should be set.
 	//////////
+	assert.Equal(t, errA, idb.ErrorNotInitialized)
+	assert.Equal(t, errL, idb.ErrorNotInitialized)
 	assert.Equal(t, uint64(0), roundA)
 	assert.Equal(t, uint64(0), roundL)
 }
@@ -91,7 +62,7 @@ func TestMaxRoundEmptyMetastate(t *testing.T) {
 	///////////
 	// Given // The database has the metastate set but the account_round is missing.
 	///////////
-	db, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	db, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 	pg.Exec(`INSERT INTO metastate (k, v) values ('state', '{}')`)
 
@@ -99,11 +70,11 @@ func TestMaxRoundEmptyMetastate(t *testing.T) {
 	// When // We request the max round.
 	//////////
 	round, err := db.GetMaxRoundAccounted()
-	assert.NoError(t, err)
 
 	//////////
-	// Then // There should be no error and we return that there are zero rounds.
+	// Then // The error message should be set.
 	//////////
+	assert.Equal(t, err, idb.ErrorNotInitialized)
 	assert.Equal(t, uint64(0), round)
 }
 
@@ -114,7 +85,7 @@ func TestMaxRound(t *testing.T) {
 	///////////
 	// Given // The database has the metastate set normally.
 	///////////
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 	db.Exec(`INSERT INTO metastate (k, v) values ($1, $2)`, "state", "{\"account_round\":123454321}")
 	db.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`, 543212345)
@@ -134,7 +105,7 @@ func TestMaxRound(t *testing.T) {
 	assert.Equal(t, uint64(543212345), roundL)
 }
 
-func assertAccountAsset(t *testing.T, db *sql.DB, addr types.Address, assetid uint64, frozen bool, amount uint64) {
+func assertAccountAsset(t *testing.T, db *sql.DB, addr sdk_types.Address, assetid uint64, frozen bool, amount uint64) {
 	var row *sql.Row
 	var f bool
 	var a uint64
@@ -151,7 +122,7 @@ func TestAssetCloseReopenTransfer(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -162,10 +133,10 @@ func TestAssetCloseReopenTransfer(t *testing.T) {
 	// Given // A round scenario requiring subround accounting: AccountA is funded, closed, opts back, and funded again.
 	///////////
 	_, createAsset := test.MakeAssetConfigOrPanic(test.Round, 0, assetid, total, uint64(6), false, "icicles", "frozen coin", "http://antarctica.com", test.AccountD)
-	_, fundMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, types.ZeroAddress)
+	_, fundMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, sdk_types.ZeroAddress)
 	_, closeMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 1000, test.AccountA, test.AccountB, test.AccountC)
-	_, optinMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, types.ZeroAddress)
-	_, payMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, types.ZeroAddress)
+	_, optinMain := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress)
+	_, payMain := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, sdk_types.ZeroAddress)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -179,7 +150,7 @@ func TestAssetCloseReopenTransfer(t *testing.T) {
 	//////////
 	// When // We commit the round accounting to the database.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -201,7 +172,7 @@ func TestDefaultFrozenAndCache(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -212,8 +183,8 @@ func TestDefaultFrozenAndCache(t *testing.T) {
 	///////////
 	_, createAssetFrozen := test.MakeAssetConfigOrPanic(test.Round, 0, assetid, total, uint64(6), true, "icicles", "frozen coin", "http://antarctica.com", test.AccountA)
 	_, createAssetNotFrozen := test.MakeAssetConfigOrPanic(test.Round, 0, assetid+1, total, uint64(6), false, "icicles", "frozen coin", "http://antarctica.com", test.AccountA)
-	_, optinB1 := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, types.ZeroAddress)
-	_, optinB2 := test.MakeAssetTxnOrPanic(test.Round, assetid+1, 0, test.AccountB, test.AccountB, types.ZeroAddress)
+	_, optinB1 := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, sdk_types.ZeroAddress)
+	_, optinB2 := test.MakeAssetTxnOrPanic(test.Round, assetid+1, 0, test.AccountB, test.AccountB, sdk_types.ZeroAddress)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -226,7 +197,7 @@ func TestDefaultFrozenAndCache(t *testing.T) {
 	//////////
 	// When // We commit the round accounting to the database.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -247,7 +218,7 @@ func TestInitializeFrozenCache(t *testing.T) {
 	defer shutdownFunc()
 
 	// Initialize DB by creating one of these things.
-	_, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	_, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	// Add some assets
@@ -255,7 +226,7 @@ func TestInitializeFrozenCache(t *testing.T) {
 	db.Exec(`INSERT INTO asset (index, creator_addr, params) values ($1, $2, $3)`, 2, test.AccountA[:], `{"df":false}`)
 	db.Exec(`INSERT INTO asset (index, creator_addr, params) values ($1, $2, $3)`, 3, test.AccountA[:], `{}`)
 
-	pdb, err := OpenPostgres(connStr, nil, nil)
+	pdb, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -272,7 +243,7 @@ func TestReCreateAssetHolding(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -299,8 +270,8 @@ func TestReCreateAssetHolding(t *testing.T) {
 		// Given // A new asset with default-frozen, AccountB opts-in and has its frozen state toggled.
 		/////////// Then AccountB opts-out then opts-in again.
 		_, createAssetFrozen := test.MakeAssetConfigOrPanic(round, 0, aid, total, uint64(6), testcase.frozen, "icicles", "frozen coin", "http://antarctica.com", test.AccountA)
-		_, optinB := test.MakeAssetTxnOrPanic(round, aid, 0, test.AccountB, test.AccountB, types.ZeroAddress)
-		_, unfreezeB := test.MakeAssetFreezeOrPanic(round, aid, !testcase.frozen, test.AccountB)
+		_, optinB := test.MakeAssetTxnOrPanic(round, aid, 0, test.AccountB, test.AccountB, sdk_types.ZeroAddress)
+		_, unfreezeB := test.MakeAssetFreezeOrPanic(round, aid, !testcase.frozen, test.AccountB, test.AccountB)
 		_, optoutB := test.MakeAssetTxnOrPanic(round, aid, 0, test.AccountB, test.AccountC, test.AccountD)
 
 		cache, err := pdb.GetDefaultFrozen()
@@ -315,7 +286,7 @@ func TestReCreateAssetHolding(t *testing.T) {
 		//////////
 		// When // We commit the round accounting to the database.
 		//////////
-		err = pdb.CommitRoundAccounting(state.RoundUpdates, round, &itypes.Block{})
+		err = pdb.CommitRoundAccounting(state.RoundUpdates, round, &types.Block{})
 		assert.NoError(t, err, "failed to commit")
 
 		//////////
@@ -338,12 +309,12 @@ func TestNoopOptins(t *testing.T) {
 	// create asst
 	//db.Exec(`INSERT INTO asset (index, creator_addr, params) values ($1, $2, $3)`, assetid, test.AccountA[:], `{"df":true}`)
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	_, createAsset := test.MakeAssetConfigOrPanic(test.Round, 0, assetid, uint64(1000000), uint64(6), true, "icicles", "frozen coin", "http://antarctica.com", test.AccountD)
-	_, optinB := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, types.ZeroAddress)
-	_, unfreezeB := test.MakeAssetFreezeOrPanic(test.Round, assetid, false, test.AccountB)
+	_, optinB := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, sdk_types.ZeroAddress)
+	_, unfreezeB := test.MakeAssetFreezeOrPanic(test.Round, assetid, false, test.AccountB, test.AccountB)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -356,7 +327,7 @@ func TestNoopOptins(t *testing.T) {
 	//////////
 	// When // We commit the round accounting to the database.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -371,7 +342,7 @@ func TestMultipleWriters(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	amt := uint64(10000)
@@ -380,7 +351,7 @@ func TestMultipleWriters(t *testing.T) {
 	// Given // Send amt to AccountA
 	///////////
 	_, payAccountA := test.MakePayTxnRowOrPanic(test.Round, 1000, amt, 0, 0, 0, 0, test.AccountD,
-		test.AccountA, types.ZeroAddress, types.ZeroAddress)
+		test.AccountA, sdk_types.ZeroAddress, sdk_types.ZeroAddress)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -399,7 +370,7 @@ func TestMultipleWriters(t *testing.T) {
 		go func() {
 			defer wg.Done()
 			<-start
-			errors <- pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+			errors <- pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 		}()
 	}
 	close(start)
@@ -428,10 +399,12 @@ func TestMultipleWriters(t *testing.T) {
 
 // TestBlockWithTransactions tests that the block with transactions endpoint works.
 func TestBlockWithTransactions(t *testing.T) {
+	var err error
+
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -442,27 +415,32 @@ func TestBlockWithTransactions(t *testing.T) {
 	// Given // A block at round test.Round with 5 transactions.
 	///////////
 	tx1, row1 := test.MakeAssetConfigOrPanic(test.Round, 0, assetid, total, uint64(6), false, "icicles", "frozen coin", "http://antarctica.com", test.AccountD)
-	tx2, row2 := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, types.ZeroAddress)
+	tx2, row2 := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, sdk_types.ZeroAddress)
 	tx3, row3 := test.MakeAssetTxnOrPanic(test.Round, assetid, 1000, test.AccountA, test.AccountB, test.AccountC)
-	tx4, row4 := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, types.ZeroAddress)
-	tx5, row5 := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, types.ZeroAddress)
-	txns := []*types.SignedTxnWithAD{tx1, tx2, tx3, tx4, tx5}
+	tx4, row4 := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress)
+	tx5, row5 := test.MakeAssetTxnOrPanic(test.Round, assetid, amt, test.AccountD, test.AccountA, sdk_types.ZeroAddress)
+	txns := []*sdk_types.SignedTxnWithAD{tx1, tx2, tx3, tx4, tx5}
 	txnRows := []*idb.TxnRow{row1, row2, row3, row4, row5}
 
-	db.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`, test.Round)
+	_, err = db.Exec(`INSERT INTO metastate (k, v) values ($1, $2)`, "state", `{"account_round": 11}`)
+	require.NoError(t, err)
+	_, err = db.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`, test.Round)
+	require.NoError(t, err)
 	for i := range txns {
-		db.Exec(`INSERT INTO txn (round, intra, typeenum, asset, txid, txnbytes, txn) VALUES ($1, $2, $3, $4, $5, $6, $7)`, test.Round, i, 0, 0, crypto.TransactionID(txns[i].Txn), txnRows[i].TxnBytes, "{}")
+		_, err = db.Exec(`INSERT INTO txn (round, intra, typeenum, asset, txid, txnbytes, txn) VALUES ($1, $2, $3, $4, $5, $6, $7)`, test.Round, i, 0, 0, crypto.TransactionID(txns[i].Txn), txnRows[i].TxnBytes, "{}")
+		require.NoError(t, err)
 	}
 
 	//////////
 	// When // We call GetBlock and Transactions
 	//////////
 	_, blockTxn, err := pdb.GetBlock(context.Background(), test.Round, idb.GetBlockOptions{Transactions: true})
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	round := test.Round
 	txnRow, _ := pdb.Transactions(context.Background(), idb.TransactionFilter{Round: &round})
 	transactionsTxn := make([]idb.TxnRow, 0)
 	for row := range txnRow {
+		require.NoError(t, row.Error)
 		transactionsTxn = append(transactionsTxn, row)
 	}
 
@@ -481,21 +459,21 @@ func TestRekeyBasic(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	///////////
 	// Given // Send rekey transaction
 	///////////
 	_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-		test.AccountA, types.ZeroAddress, test.AccountB)
+		test.AccountA, sdk_types.ZeroAddress, test.AccountB)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
 	state := getAccounting(test.Round, cache)
 	state.AddTransaction(txnRow)
 
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -506,7 +484,7 @@ func TestRekeyBasic(t *testing.T) {
 	err = row.Scan(&accountDataStr)
 	assert.NoError(t, err, "querying account data")
 
-	var ad itypes.AccountData
+	var ad types.AccountData
 	err = json.Decode(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
 	assert.Equal(t, test.AccountB, ad.SpendingKey)
@@ -516,7 +494,7 @@ func TestRekeyToItself(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	///////////
@@ -524,26 +502,26 @@ func TestRekeyToItself(t *testing.T) {
 	///////////
 	{
 		_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-			test.AccountA, types.ZeroAddress, test.AccountB)
+			test.AccountA, sdk_types.ZeroAddress, test.AccountB)
 
 		cache, err := pdb.GetDefaultFrozen()
 		assert.NoError(t, err)
 		state := getAccounting(test.Round, cache)
 		state.AddTransaction(txnRow)
 
-		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 		assert.NoError(t, err, "failed to commit")
 	}
 	{
 		_, txnRow := test.MakePayTxnRowOrPanic(test.Round+1, 1000, 0, 0, 0, 0, 0, test.AccountA,
-			test.AccountA, types.ZeroAddress, test.AccountA)
+			test.AccountA, sdk_types.ZeroAddress, test.AccountA)
 
 		cache, err := pdb.GetDefaultFrozen()
 		assert.NoError(t, err)
 		state := getAccounting(test.Round+1, cache)
 		state.AddTransaction(txnRow)
 
-		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round+1, &itypes.Block{})
+		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round+1, &types.Block{})
 		assert.NoError(t, err, "failed to commit")
 	}
 
@@ -555,17 +533,17 @@ func TestRekeyToItself(t *testing.T) {
 	err = row.Scan(&accountDataStr)
 	assert.NoError(t, err, "querying account data")
 
-	var ad itypes.AccountData
+	var ad types.AccountData
 	err = json.Decode(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
-	assert.Equal(t, types.ZeroAddress, ad.SpendingKey)
+	assert.Equal(t, sdk_types.ZeroAddress, ad.SpendingKey)
 }
 
 func TestRekeyThreeTimesInSameRound(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	///////////
@@ -577,21 +555,21 @@ func TestRekeyThreeTimesInSameRound(t *testing.T) {
 
 	{
 		_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-			test.AccountA, types.ZeroAddress, test.AccountB)
+			test.AccountA, sdk_types.ZeroAddress, test.AccountB)
 		state.AddTransaction(txnRow)
 	}
 	{
 		_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-			test.AccountA, types.ZeroAddress, types.ZeroAddress)
+			test.AccountA, sdk_types.ZeroAddress, sdk_types.ZeroAddress)
 		state.AddTransaction(txnRow)
 	}
 	{
 		_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-			test.AccountA, types.ZeroAddress, test.AccountC)
+			test.AccountA, sdk_types.ZeroAddress, test.AccountC)
 		state.AddTransaction(txnRow)
 	}
 
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -602,7 +580,7 @@ func TestRekeyThreeTimesInSameRound(t *testing.T) {
 	err = row.Scan(&accountDataStr)
 	assert.NoError(t, err, "querying account data")
 
-	var ad itypes.AccountData
+	var ad types.AccountData
 	err = json.Decode(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
 	assert.Equal(t, test.AccountC, ad.SpendingKey)
@@ -612,14 +590,14 @@ func TestRekeyToItselfHasNotBeenRekeyed(t *testing.T) {
 	_, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	///////////
 	// Given // Send rekey transaction
 	///////////
 	_, txnRow := test.MakePayTxnRowOrPanic(test.Round, 1000, 0, 0, 0, 0, 0, test.AccountA,
-		test.AccountA, types.ZeroAddress, types.ZeroAddress)
+		test.AccountA, sdk_types.ZeroAddress, sdk_types.ZeroAddress)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -629,7 +607,7 @@ func TestRekeyToItselfHasNotBeenRekeyed(t *testing.T) {
 	//////////
 	// Then // No error when committing to the DB.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 }
 
@@ -638,7 +616,7 @@ func TestIgnoreDefaultFrozenConfigUpdate(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -649,7 +627,7 @@ func TestIgnoreDefaultFrozenConfigUpdate(t *testing.T) {
 	///////////
 	_, createAssetNotFrozen := test.MakeAssetConfigOrPanic(test.Round, 0, assetid, total, uint64(6), false, "icicles", "frozen coin", "http://antarctica.com", test.AccountA)
 	_, modifyAssetToFrozen := test.MakeAssetConfigOrPanic(test.Round, assetid, assetid, total, uint64(6), true, "icicles", "frozen coin", "http://antarctica.com", test.AccountA)
-	_, optin := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, types.ZeroAddress)
+	_, optin := test.MakeAssetTxnOrPanic(test.Round, assetid, 0, test.AccountB, test.AccountB, sdk_types.ZeroAddress)
 
 	cache, err := pdb.GetDefaultFrozen()
 	assert.NoError(t, err)
@@ -661,7 +639,7 @@ func TestIgnoreDefaultFrozenConfigUpdate(t *testing.T) {
 	//////////
 	// When // We commit the round accounting to the database.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -677,7 +655,7 @@ func TestZeroTotalAssetCreate(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	assetid := uint64(2222)
@@ -696,7 +674,7 @@ func TestZeroTotalAssetCreate(t *testing.T) {
 	//////////
 	// When // We commit the round accounting to the database.
 	//////////
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	//////////
@@ -720,7 +698,7 @@ func assertAssetDates(t *testing.T, db *sql.DB, assetID uint64, deleted sql.Null
 	assert.Equal(t, closedAt, retClosedAt)
 }
 
-func assertAssetHoldingDates(t *testing.T, db *sql.DB, address types.Address, assetID uint64, deleted sql.NullBool, createdAt sql.NullInt64, closedAt sql.NullInt64) {
+func assertAssetHoldingDates(t *testing.T, db *sql.DB, address sdk_types.Address, assetID uint64, deleted sql.NullBool, createdAt sql.NullInt64, closedAt sql.NullInt64) {
 	row := db.QueryRow(
 		"SELECT deleted, created_at, closed_at FROM account_asset WHERE "+
 			"addr = $1 AND assetid = $2",
@@ -741,7 +719,7 @@ func TestDestroyAssetBasic(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	cache, err := pdb.GetDefaultFrozen()
@@ -758,7 +736,7 @@ func TestDestroyAssetBasic(t *testing.T) {
 		err := state.AddTransaction(txnRow)
 		assert.NoError(t, err)
 
-		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 		assert.NoError(t, err, "failed to commit")
 	}
 	// Destroy an asset.
@@ -769,7 +747,7 @@ func TestDestroyAssetBasic(t *testing.T) {
 		err := state.AddTransaction(txnRow)
 		assert.NoError(t, err)
 
-		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round+1, &itypes.Block{})
+		err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round+1, &types.Block{})
 		assert.NoError(t, err, "failed to commit")
 	}
 
@@ -790,7 +768,7 @@ func TestDestroyAssetZeroSupply(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	cache, err := pdb.GetDefaultFrozen()
@@ -817,7 +795,7 @@ func TestDestroyAssetZeroSupply(t *testing.T) {
 		assert.NoError(t, err)
 	}
 
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	// Check that the asset is deleted.
@@ -837,7 +815,7 @@ func TestDestroyAssetDeleteCreatorsHolding(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	pdb, err := idb.IndexerDbByName("postgres", connStr, nil, nil)
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
 	cache, err := pdb.GetDefaultFrozen()
@@ -850,15 +828,15 @@ func TestDestroyAssetDeleteCreatorsHolding(t *testing.T) {
 	// Create an asset.
 	{
 		// Create a transaction where all special addresses are different from creator's address.
-		txn := types.SignedTxnWithAD{
-			SignedTxn: types.SignedTxn{
-				Txn: types.Transaction{
+		txn := sdk_types.SignedTxnWithAD{
+			SignedTxn: sdk_types.SignedTxn{
+				Txn: sdk_types.Transaction{
 					Type: "acfg",
-					Header: types.Header{
+					Header: sdk_types.Header{
 						Sender: test.AccountA,
 					},
-					AssetConfigTxnFields: types.AssetConfigTxnFields{
-						AssetParams: types.AssetParams{
+					AssetConfigTxnFields: sdk_types.AssetConfigTxnFields{
+						AssetParams: sdk_types.AssetParams{
 							Manager:  test.AccountB,
 							Reserve:  test.AccountB,
 							Freeze:   test.AccountB,
@@ -880,7 +858,7 @@ func TestDestroyAssetDeleteCreatorsHolding(t *testing.T) {
 	// Another account opts in.
 	{
 		_, txnRow := test.MakeAssetTxnOrPanic(test.Round, assetID, 0, test.AccountC,
-			test.AccountC, types.ZeroAddress)
+			test.AccountC, sdk_types.ZeroAddress)
 		state.AddTransaction(txnRow)
 	}
 	// Destroy an asset.
@@ -889,7 +867,7 @@ func TestDestroyAssetDeleteCreatorsHolding(t *testing.T) {
 		state.AddTransaction(txnRow)
 	}
 
-	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &itypes.Block{})
+	err = pdb.CommitRoundAccounting(state.RoundUpdates, test.Round, &types.Block{})
 	assert.NoError(t, err, "failed to commit")
 
 	// Check that the creator's asset holding is deleted.
@@ -906,12 +884,39 @@ func TestDestroyAssetDeleteCreatorsHolding(t *testing.T) {
 
 	// Check that the manager does not have an asset holding.
 	{
-		row := db.QueryRow("SELECT COUNT(*) FROM account_asset WHERE addr = $1", test.AccountB[:])
+		count := queryInt(db, "SELECT COUNT(*) FROM account_asset WHERE addr = $1", test.AccountB[:])
+		assert.Equal(t, 0, count)
+	}
+}
 
-		var count int64
-		err := row.Scan(&count)
-		assert.NoError(t, err)
+// Test that block import adds the freeze/sender accounts to txn_participation.
+func TestAssetFreezeTxnParticipation(t *testing.T) {
+	db, connStr, shutdownFunc := setupPostgres(t)
+	defer shutdownFunc()
+	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
+	assert.NoError(t, err)
+	blockImporter := importer.NewDBImporter(pdb)
 
-		assert.Equal(t, int64(0), count)
-	}
+	///////////
+	// Given // A block containing an asset freeze txn
+	///////////
+
+	// Create a block with freeze txn
+	freeze, _ := test.MakeAssetFreezeOrPanic(test.Round, 1234, true, test.AccountA, test.AccountB)
+	block := test.MakeBlockForTxns(test.Round, freeze)
+
+	//////////
+	// When // We import the block.
+	//////////
+	txnCount, err := blockImporter.ImportDecodedBlock(&block)
+	assert.NoError(t, err, "failed to import")
+	assert.Equal(t, 1, txnCount)
+
+	//////////
+	// Then // Both accounts should have an entry in the txn_participation table.
+	//////////
+	acctACount := queryInt(db, "SELECT COUNT(*) FROM txn_participation WHERE addr = $1", test.AccountA[:])
+	acctBCount := queryInt(db, "SELECT COUNT(*) FROM txn_participation WHERE addr = $1", test.AccountB[:])
+	assert.Equal(t, 1, acctACount)
+	assert.Equal(t, 1, acctBCount)
 }
diff --git a/idb/postgres/postgres_migrations_integration_test.go b/idb/postgres/postgres_migrations_integration_test.go
new file mode 100644
index 0000000..10ec4b8
--- /dev/null
+++ b/idb/postgres/postgres_migrations_integration_test.go
@@ -0,0 +1,237 @@
+package postgres
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/algorand/go-algorand-sdk/encoding/json"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
+
+	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util/test"
+)
+
+func nextMigrationNum(t *testing.T, db *IndexerDb) int {
+	j, err := db.getMetastate(migrationMetastateKey)
+	assert.NoError(t, err)
+
+	assert.True(t, len(j) > 0)
+
+	var state MigrationState
+	err = json.Decode([]byte(j), &state)
+	assert.NoError(t, err)
+
+	return state.NextMigration
+}
+
+func TestFixFreezeLookupMigration(t *testing.T) {
+	db, shutdownFunc := setupIdb(t)
+	defer shutdownFunc()
+
+	var sender types.Address
+	var faddr types.Address
+
+	sender[0] = 0x01
+	faddr[0] = 0x02
+
+	///////////
+	// Given // A block containing an asset freeze txn has been imported.
+	///////////
+	freeze, _ := test.MakeAssetFreezeOrPanic(test.Round, 1234, true, sender, faddr)
+	importTxns(t, db, test.Round, freeze)
+
+	//////////
+	// When // We truncate the txn_participation table and run our migration
+	//////////
+	db.db.Exec("TRUNCATE txn_participation")
+	FixFreezeLookupMigration(db, &MigrationState{NextMigration: 12})
+
+	//////////
+	// Then // The sender is still deleted, but the freeze addr should be back.
+	//////////
+	senderCount := queryInt(db.db, "SELECT COUNT(*) FROM txn_participation WHERE addr = $1", sender[:])
+	faddrCount := queryInt(db.db, "SELECT COUNT(*) FROM txn_participation WHERE addr = $1", faddr[:])
+	assert.Equal(t, 0, senderCount)
+	assert.Equal(t, 1, faddrCount)
+}
+
+// Test that ClearAccountDataMigration() clears account data for closed accounts.
+func TestClearAccountDataMigrationClosedAccounts(t *testing.T) {
+	db, shutdownFunc := setupIdb(t)
+	defer shutdownFunc()
+
+	// Rekey account A.
+	{
+		stxn, txnRow := test.MakePayTxnRowOrPanic(
+			test.Round, 0, 0, 0, 0, 0, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress,
+			test.AccountB)
+
+		importTxns(t, db, test.Round, stxn)
+		accountTxns(t, db, test.Round, txnRow)
+	}
+
+	// Close account A without deleting account data.
+	{
+		query := "UPDATE account SET deleted = true, closed_at = $1 WHERE addr = $2"
+		_, err := db.db.Exec(query, test.Round+1, test.AccountA[:])
+		assert.NoError(t, err)
+	}
+
+	// Run migration.
+	err := ClearAccountDataMigration(db, &MigrationState{})
+	assert.NoError(t, err)
+
+	// Check that account A has no account data.
+	opts := idb.AccountQueryOptions{
+		EqualToAddress: test.AccountA[:],
+		IncludeDeleted: true,
+	}
+	ch, _ := db.GetAccounts(context.Background(), opts)
+	accountRow, ok := <-ch
+	assert.True(t, ok)
+	assert.NoError(t, accountRow.Error)
+	account := accountRow.Account
+	assert.Nil(t, account.AuthAddr)
+}
+
+// Test that ClearAccountDataMigration() clears account data that was set before account was closed.
+func TestClearAccountDataMigrationClearsReopenedAccounts(t *testing.T) {
+	db, shutdownFunc := setupIdb(t)
+	defer shutdownFunc()
+
+	// Create account A.
+	{
+		stxn, txnRow := test.MakePayTxnRowOrPanic(
+			test.Round, 0, 0, 0, 0, 0, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress,
+			sdk_types.ZeroAddress)
+
+		importTxns(t, db, test.Round, stxn)
+		accountTxns(t, db, test.Round, txnRow)
+	}
+
+	// Make rekey and keyreg transactions for account A.
+	{
+		stxn0, txnRow0 := test.MakePayTxnRowOrPanic(
+			test.Round+1, 0, 0, 0, 0, 0, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress,
+			test.AccountB)
+		stxn1, txnRow1 := test.MakeSimpleKeyregOnlineTxn(test.Round+1, test.AccountA)
+
+		importTxns(t, db, test.Round+1, stxn0, stxn1)
+		accountTxns(t, db, test.Round+1, txnRow0, txnRow1)
+	}
+
+	// Check that account A is online, has auth-addr and participation info.
+	opts := idb.AccountQueryOptions{
+		EqualToAddress: test.AccountA[:],
+	}
+	{
+		ch, _ := db.GetAccounts(context.Background(), opts)
+		accountRow, ok := <-ch
+		assert.True(t, ok)
+		assert.NoError(t, accountRow.Error)
+		account := accountRow.Account
+		assert.NotNil(t, account.AuthAddr)
+		assert.NotNil(t, account.Participation)
+		assert.Equal(t, "Online", account.Status)
+	}
+
+	// Simulate closing and reopening of account A.
+	{
+		query := "UPDATE account SET deleted = false, closed_at = $1 WHERE addr = $2"
+		_, err := db.db.Exec(query, test.Round+2, test.AccountA[:])
+		assert.NoError(t, err)
+	}
+
+	// Run migration.
+	err := ClearAccountDataMigration(db, &MigrationState{})
+	assert.NoError(t, err)
+
+	// Check that account A is offline and has no account data.
+	{
+		ch, _ := db.GetAccounts(context.Background(), opts)
+		accountRow, ok := <-ch
+		assert.True(t, ok)
+		assert.NoError(t, accountRow.Error)
+		account := accountRow.Account
+		assert.Nil(t, account.AuthAddr)
+		assert.Nil(t, account.Participation)
+		assert.Equal(t, "Offline", account.Status)
+	}
+}
+
+// Test that ClearAccountDataMigration() does not clear account data because is was updated after
+// account was closed.
+func TestClearAccountDataMigrationDoesNotClear(t *testing.T) {
+	db, shutdownFunc := setupIdb(t)
+	defer shutdownFunc()
+
+	// Create account A.
+	{
+		stxn, txnRow := test.MakePayTxnRowOrPanic(
+			test.Round, 0, 0, 0, 0, 0, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress,
+			sdk_types.ZeroAddress)
+
+		importTxns(t, db, test.Round, stxn)
+		accountTxns(t, db, test.Round, txnRow)
+	}
+
+	// Simulate closing and reopening of account A.
+	{
+		query := "UPDATE account SET deleted = false, closed_at = $1 WHERE addr = $2"
+		_, err := db.db.Exec(query, test.Round+1, test.AccountA[:])
+		assert.NoError(t, err)
+	}
+
+	// Account A rekey and keyreg.
+	{
+		stxn0, txnRow0 := test.MakePayTxnRowOrPanic(
+			test.Round+2, 0, 0, 0, 0, 0, 0, test.AccountA, test.AccountA, sdk_types.ZeroAddress,
+			test.AccountB)
+		stxn1, txnRow1 := test.MakeSimpleKeyregOnlineTxn(test.Round+2, test.AccountA)
+
+		importTxns(t, db, test.Round+2, stxn0, stxn1)
+		accountTxns(t, db, test.Round+2, txnRow0, txnRow1)
+	}
+
+	// A safety check.
+	{
+		accounts, err := getAccounts(db.db)
+		assert.NoError(t, err)
+		assert.Equal(t, 1, len(accounts))
+	}
+
+	// Run migration.
+	err := ClearAccountDataMigration(db, &MigrationState{})
+	assert.NoError(t, err)
+
+	// Check that account A is online and has auth addr and keyreg data.
+	opts := idb.AccountQueryOptions{
+		EqualToAddress: test.AccountA[:],
+	}
+	ch, _ := db.GetAccounts(context.Background(), opts)
+	accountRow, ok := <-ch
+	assert.True(t, ok)
+	assert.NoError(t, accountRow.Error)
+	account := accountRow.Account
+	assert.NotNil(t, account.AuthAddr)
+	assert.NotNil(t, account.Participation)
+	assert.Equal(t, "Online", account.Status)
+}
+
+// Test that ClearAccountDataMigration() increments the next migration number.
+func TestClearAccountDataMigrationIncMigrationNum(t *testing.T) {
+	db, shutdownFunc := setupIdb(t)
+	defer shutdownFunc()
+
+	// Run migration.
+	state := MigrationState{NextMigration: 13}
+	err := ClearAccountDataMigration(db, &state)
+	assert.NoError(t, err)
+
+	assert.Equal(t, 14, state.NextMigration)
+	newNum := nextMigrationNum(t, db)
+	assert.Equal(t, 14, newNum)
+}
diff --git a/idb/postgres/postgres_test.go b/idb/postgres/postgres_test.go
index dcac825..9f582b2 100644
--- a/idb/postgres/postgres_test.go
+++ b/idb/postgres/postgres_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/types"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -33,7 +34,7 @@ func TestAllMigrations(t *testing.T) {
 			})
 
 			// This automatically runs migrations
-			pdb, err := openPostgres(db, &idb.IndexerDbOptions{
+			pdb, err := openPostgres(db, idb.IndexerDbOptions{
 				ReadOnly: false,
 			}, nil)
 			require.NoError(t, err)
@@ -74,9 +75,10 @@ func TestNoMigrationsNeeded(t *testing.T) {
 	})
 
 	// This automatically runs migraions
-	pdb, err := openPostgres(db, &idb.IndexerDbOptions{
+	pdb, err := openPostgres(db, idb.IndexerDbOptions{
 		ReadOnly: false,
 	}, nil)
+	assert.NoError(t, err)
 
 	// Just need a moment for the go routine to get started
 	time.Sleep(100 * time.Millisecond)
diff --git a/test/common.sh b/test/common.sh
index ee4a914..27c8ca4 100755
--- a/test/common.sh
+++ b/test/common.sh
@@ -191,7 +191,11 @@ function suppress() {
 # $2 - if set, puts in read-only mode
 function start_indexer_with_connection_string() {
   if [ ! -z $2 ]; then
+    # strictly read-only
     RO="--no-algod"
+  else
+    # we may start up from canned data, but need to update for the current running binary.
+    RO="--allow-migration"
   fi
   ALGORAND_DATA= ../cmd/algorand-indexer/algorand-indexer daemon \
     -S $NET "$RO" \
diff --git a/util/test/account_testutil.go b/util/test/account_testutil.go
index 3ba74aa..34f4f98 100644
--- a/util/test/account_testutil.go
+++ b/util/test/account_testutil.go
@@ -2,10 +2,12 @@ package test
 
 import (
 	"fmt"
+
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	"github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/types"
 )
 
 // Round is the round used in pre-made transactions.
@@ -26,31 +28,34 @@ var (
 	RewardAddr = DecodeAddressOrPanic("4C3S3A5II6AYMEADSW7EVL7JAKVU2ASJMMJAGVUROIJHYMS6B24NCXVEWM")
 
 	// OpenMainStxn is a premade signed transaction which may be useful in tests.
-	OpenMainStxn *types.SignedTxnWithAD
+	OpenMainStxn *sdk_types.SignedTxnWithAD
 	// OpenMain is a premade TxnRow which may be useful in tests.
 	OpenMain *idb.TxnRow
 
 	// CloseMainToBCStxn is a premade signed transaction which may be useful in tests.
-	CloseMainToBCStxn *types.SignedTxnWithAD
+	CloseMainToBCStxn *sdk_types.SignedTxnWithAD
 	// CloseMainToBC is a premade TxnRow which may be useful in tests.
 	CloseMainToBC *idb.TxnRow
+
+	// Proto is a fake protocol version.
+	Proto = "future"
 )
 
 func init() {
 	OpenMainStxn, OpenMain = MakePayTxnRowOrPanic(Round, 1000, 10234, 0, 111, 1111, 0, AccountC,
-		AccountA, types.ZeroAddress, types.ZeroAddress)
+		AccountA, sdk_types.ZeroAddress, sdk_types.ZeroAddress)
 	// CloseMainToBCStxn and CloseMainToBC are premade transactions which may be useful in tests.
 	CloseMainToBCStxn, CloseMainToBC = MakePayTxnRowOrPanic(Round, 1000, 1234, 9111, 0, 111, 111,
-		AccountA, AccountC, AccountB, types.ZeroAddress)
+		AccountA, AccountC, AccountB, sdk_types.ZeroAddress)
 }
 
 // DecodeAddressOrPanic is a helper to ensure addresses are initialized.
-func DecodeAddressOrPanic(addr string) types.Address {
+func DecodeAddressOrPanic(addr string) sdk_types.Address {
 	if addr == "" {
-		return types.ZeroAddress
+		return sdk_types.ZeroAddress
 	}
 
-	result, err := types.DecodeAddress(addr)
+	result, err := sdk_types.DecodeAddress(addr)
 	if err != nil {
 		panic(fmt.Sprintf("Failed to decode address: '%s'", addr))
 	}
@@ -58,20 +63,20 @@ func DecodeAddressOrPanic(addr string) types.Address {
 }
 
 // MakeAssetConfigOrPanic is a helper to ensure test asset config are initialized.
-func MakeAssetConfigOrPanic(round, configid, assetid, total, decimals uint64, defaultFrozen bool, unitName, assetName, url string, addr types.Address) (*types.SignedTxnWithAD, *idb.TxnRow) {
-	txn := types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
+func MakeAssetConfigOrPanic(round, configid, assetid, total, decimals uint64, defaultFrozen bool, unitName, assetName, url string, addr sdk_types.Address) (*sdk_types.SignedTxnWithAD, *idb.TxnRow) {
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
 				Type: "acfg",
-				Header: types.Header{
+				Header: sdk_types.Header{
 					Sender:     addr,
-					Fee:        types.MicroAlgos(1000),
-					FirstValid: types.Round(round),
-					LastValid:  types.Round(round),
+					Fee:        sdk_types.MicroAlgos(1000),
+					FirstValid: sdk_types.Round(round),
+					LastValid:  sdk_types.Round(round),
 				},
-				AssetConfigTxnFields: types.AssetConfigTxnFields{
-					ConfigAsset: types.AssetIndex(configid),
-					AssetParams: types.AssetParams{
+				AssetConfigTxnFields: sdk_types.AssetConfigTxnFields{
+					ConfigAsset: sdk_types.AssetIndex(configid),
+					AssetParams: sdk_types.AssetParams{
 						Total:         total,
 						Decimals:      uint32(decimals),
 						DefaultFrozen: defaultFrozen,
@@ -99,20 +104,20 @@ func MakeAssetConfigOrPanic(round, configid, assetid, total, decimals uint64, de
 }
 
 // MakeAssetFreezeOrPanic create an asset freeze/unfreeze transaction.
-func MakeAssetFreezeOrPanic(round, assetid uint64, frozen bool, addr types.Address) (*types.SignedTxnWithAD, *idb.TxnRow) {
-	txn := types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
+func MakeAssetFreezeOrPanic(round, assetid uint64, frozen bool, sender, freezeAccount sdk_types.Address) (*sdk_types.SignedTxnWithAD, *idb.TxnRow) {
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
 				Type: "afrz",
-				Header: types.Header{
-					Sender:     addr,
-					Fee:        types.MicroAlgos(1000),
-					FirstValid: types.Round(round),
-					LastValid:  types.Round(round),
+				Header: sdk_types.Header{
+					Sender:     sender,
+					Fee:        sdk_types.MicroAlgos(1000),
+					FirstValid: sdk_types.Round(round),
+					LastValid:  sdk_types.Round(round),
 				},
-				AssetFreezeTxnFields: types.AssetFreezeTxnFields{
-					FreezeAccount: addr,
-					FreezeAsset:   types.AssetIndex(assetid),
+				AssetFreezeTxnFields: sdk_types.AssetFreezeTxnFields{
+					FreezeAccount: freezeAccount,
+					FreezeAsset:   sdk_types.AssetIndex(assetid),
 					AssetFrozen:   frozen,
 				},
 			},
@@ -129,28 +134,28 @@ func MakeAssetFreezeOrPanic(round, assetid uint64, frozen bool, addr types.Addre
 }
 
 // MakeAssetTxnOrPanic creates an asset transfer transaction.
-func MakeAssetTxnOrPanic(round, assetid, amt uint64, sender, receiver, close types.Address) (*types.SignedTxnWithAD, *idb.TxnRow) {
-	txn := types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
+func MakeAssetTxnOrPanic(round, assetid, amt uint64, sender, receiver, close sdk_types.Address) (*sdk_types.SignedTxnWithAD, *idb.TxnRow) {
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
 				Type: "axfer",
-				Header: types.Header{
+				Header: sdk_types.Header{
 					Sender:     sender,
-					Fee:        types.MicroAlgos(1000),
-					FirstValid: types.Round(round),
-					LastValid:  types.Round(round),
+					Fee:        sdk_types.MicroAlgos(1000),
+					FirstValid: sdk_types.Round(round),
+					LastValid:  sdk_types.Round(round),
 				},
-				AssetTransferTxnFields: types.AssetTransferTxnFields{
-					XferAsset:   types.AssetIndex(assetid),
+				AssetTransferTxnFields: sdk_types.AssetTransferTxnFields{
+					XferAsset:   sdk_types.AssetIndex(assetid),
 					AssetAmount: amt,
 					//only used for clawback transactions
-					//AssetSender:   types.Address{},
+					//AssetSender:   sdk_types.Address{},
 					AssetReceiver: receiver,
 					AssetCloseTo:  close,
 				},
 			},
 		},
-		ApplyData: types.ApplyData{},
+		ApplyData: sdk_types.ApplyData{},
 	}
 
 	txnRow := idb.TxnRow{
@@ -162,13 +167,13 @@ func MakeAssetTxnOrPanic(round, assetid, amt uint64, sender, receiver, close typ
 }
 
 // MakeAssetDestroyTxn makes a transaction that destroys an asset.
-func MakeAssetDestroyTxn(round uint64, assetID uint64) (*types.SignedTxnWithAD, *idb.TxnRow) {
-	txn := types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
+func MakeAssetDestroyTxn(round uint64, assetID uint64) (*sdk_types.SignedTxnWithAD, *idb.TxnRow) {
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
 				Type: "acfg",
-				AssetConfigTxnFields: types.AssetConfigTxnFields{
-					ConfigAsset: types.AssetIndex(assetID),
+				AssetConfigTxnFields: sdk_types.AssetConfigTxnFields{
+					ConfigAsset: sdk_types.AssetIndex(assetID),
 				},
 			},
 		},
@@ -185,31 +190,31 @@ func MakeAssetDestroyTxn(round uint64, assetID uint64) (*types.SignedTxnWithAD,
 
 // MakePayTxnRowOrPanic creates an algo transfer transaction.
 func MakePayTxnRowOrPanic(round, fee, amt, closeAmt, sendRewards, receiveRewards,
-	closeRewards uint64, sender, receiver, close, rekeyTo types.Address) (*types.SignedTxnWithAD,
+	closeRewards uint64, sender, receiver, close, rekeyTo sdk_types.Address) (*sdk_types.SignedTxnWithAD,
 	*idb.TxnRow) {
-	txn := types.SignedTxnWithAD{
-		SignedTxn: types.SignedTxn{
-			Txn: types.Transaction{
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
 				Type: "pay",
-				Header: types.Header{
+				Header: sdk_types.Header{
 					Sender:     sender,
-					Fee:        types.MicroAlgos(fee),
-					FirstValid: types.Round(round),
-					LastValid:  types.Round(round),
+					Fee:        sdk_types.MicroAlgos(fee),
+					FirstValid: sdk_types.Round(round),
+					LastValid:  sdk_types.Round(round),
 					RekeyTo:    rekeyTo,
 				},
-				PaymentTxnFields: types.PaymentTxnFields{
+				PaymentTxnFields: sdk_types.PaymentTxnFields{
 					Receiver:         receiver,
-					Amount:           types.MicroAlgos(amt),
+					Amount:           sdk_types.MicroAlgos(amt),
 					CloseRemainderTo: close,
 				},
 			},
 		},
-		ApplyData: types.ApplyData{
-			ClosingAmount:   types.MicroAlgos(closeAmt),
-			SenderRewards:   types.MicroAlgos(sendRewards),
-			ReceiverRewards: types.MicroAlgos(receiveRewards),
-			CloseRewards:    types.MicroAlgos(closeRewards),
+		ApplyData: sdk_types.ApplyData{
+			ClosingAmount:   sdk_types.MicroAlgos(closeAmt),
+			SenderRewards:   sdk_types.MicroAlgos(sendRewards),
+			ReceiverRewards: sdk_types.MicroAlgos(receiveRewards),
+			CloseRewards:    sdk_types.MicroAlgos(closeRewards),
 		},
 	}
 
@@ -220,3 +225,63 @@ func MakePayTxnRowOrPanic(round, fee, amt, closeAmt, sendRewards, receiveRewards
 
 	return &txn, &txnRow
 }
+
+// MakeSimpleKeyregOnlineTxn creates a fake key registration transaction.
+func MakeSimpleKeyregOnlineTxn(round uint64, sender sdk_types.Address) (*sdk_types.SignedTxnWithAD, *idb.TxnRow) {
+	var votePK sdk_types.VotePK
+	votePK[0] = 1
+
+	var selectionPK sdk_types.VRFPK
+	selectionPK[0] = 2
+
+	txn := sdk_types.SignedTxnWithAD{
+		SignedTxn: sdk_types.SignedTxn{
+			Txn: sdk_types.Transaction{
+				Type: "keyreg",
+				Header: sdk_types.Header{
+					Sender:     sender,
+					FirstValid: sdk_types.Round(round),
+					LastValid:  sdk_types.Round(round),
+				},
+				KeyregTxnFields: sdk_types.KeyregTxnFields{
+					VotePK:          votePK,
+					SelectionPK:     selectionPK,
+					VoteFirst:       sdk_types.Round(round),
+					VoteLast:        sdk_types.Round(round),
+					VoteKeyDilution: 1,
+				},
+			},
+		},
+	}
+
+	txnRow := idb.TxnRow{
+		Round:    uint64(txn.Txn.FirstValid),
+		TxnBytes: msgpack.Encode(txn),
+	}
+
+	return &txn, &txnRow
+}
+
+// MakeBlockForTxns takes some transactions and constructs a block compatible with the indexer import function.
+func MakeBlockForTxns(round uint64, inputs ...*sdk_types.SignedTxnWithAD) types.EncodedBlockCert {
+	var txns []types.SignedTxnInBlock
+
+	for _, txn := range inputs {
+		txns = append(txns, types.SignedTxnInBlock{
+			SignedTxnWithAD: types.SignedTxnWithAD{SignedTxn: txn.SignedTxn},
+			HasGenesisID:    true,
+			HasGenesisHash:  true,
+		})
+	}
+
+	return types.EncodedBlockCert{
+		Block: types.Block{
+			BlockHeader: types.BlockHeader{
+				Round:        types.Round(round),
+				UpgradeState: types.UpgradeState{CurrentProtocol: types.ConsensusVersion(Proto)},
+			},
+			Payset: txns,
+		},
+		Certificate: types.Certificate{},
+	}
+}
diff --git a/util/test/testutil.go b/util/test/testutil.go
index 323861a..971cbbc 100644
--- a/util/test/testutil.go
+++ b/util/test/testutil.go
@@ -8,9 +8,10 @@ import (
 	"runtime"
 
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-	atypes "github.com/algorand/go-algorand-sdk/types"
+	sdk_types "github.com/algorand/go-algorand-sdk/types"
 	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util"
 )
 
 var quiet = false
@@ -58,10 +59,10 @@ func PrintAssetQuery(db idb.IndexerDb, q idb.AssetsQuery) {
 	count := uint64(0)
 	assetchan, _ := db.Assets(context.Background(), q)
 	for ar := range assetchan {
-		MaybeFail(ar.Error, "asset query %v\n", ar.Error)
+		util.MaybeFail(ar.Error, "asset query %v\n", ar.Error)
 		pjs, err := json.Marshal(ar.Params)
-		MaybeFail(err, "json.Marshal params %v\n", err)
-		var creator atypes.Address
+		util.MaybeFail(err, "json.Marshal params %v\n", err)
+		var creator sdk_types.Address
 		copy(creator[:], ar.Creator)
 		info("%d %s %s\n", ar.AssetID, creator.String(), pjs)
 		count++
@@ -79,9 +80,9 @@ func PrintAccountQuery(db idb.IndexerDb, q idb.AccountQueryOptions) {
 	accountchan, _ := db.GetAccounts(context.Background(), q)
 	count := uint64(0)
 	for ar := range accountchan {
-		MaybeFail(ar.Error, "GetAccounts err %v\n", ar.Error)
+		util.MaybeFail(ar.Error, "GetAccounts err %v\n", ar.Error)
 		jb, err := json.Marshal(ar.Account)
-		MaybeFail(err, "err %v\n", err)
+		util.MaybeFail(err, "err %v\n", err)
 		infoln(string(jb))
 		//fmt.Printf("%#v\n", ar.Account)
 		count++
@@ -99,10 +100,10 @@ func PrintTxnQuery(db idb.IndexerDb, q idb.TransactionFilter) {
 	rowchan, _ := db.Transactions(context.Background(), q)
 	count := uint64(0)
 	for txnrow := range rowchan {
-		MaybeFail(txnrow.Error, "err %v\n", txnrow.Error)
+		util.MaybeFail(txnrow.Error, "err %v\n", txnrow.Error)
 		var stxn types.SignedTxnWithAD
 		err := msgpack.Decode(txnrow.TxnBytes, &stxn)
-		MaybeFail(err, "decode txnbytes %v\n", err)
+		util.MaybeFail(err, "decode txnbytes %v\n", err)
 		//tjs, err := json.Marshal(stxn.Txn) // nope, ugly
 		//MaybeFail(err, "err %v\n", err)
 		//tjs := string(JSONOneLine(stxn.Txn))
@@ -117,12 +118,3 @@ func PrintTxnQuery(db idb.IndexerDb, q idb.TransactionFilter) {
 		exitValue = 1
 	}
 }
-
-// MaybeFail exits if there was an error.
-func MaybeFail(err error, errfmt string, params ...interface{}) {
-	if err == nil {
-		return
-	}
-	fmt.Fprintf(os.Stderr, errfmt, params...)
-	os.Exit(1)
-}