Repository: coinbase/rosetta-cli
Pull request #16 (2020-05-08 21:44:19)

Update rosetta-sdk-go to v0.1.10
* Update to `rosetta-sdk-go@v0.1.10`, which includes a variety of bug fixes. Take a look at this [PR](https://github.com/coinbase/rosetta-sdk-go/pull/21) for an overview of these fixes.
* Use the `rosetta-sdk-go` `parser.BalanceChanges` instead of one defined in this repo.
* Print out balance changes in the `view:block` command.



Patch:
diff --git a/cmd/view_block.go b/cmd/view_block.go
index 18bcfae..10bd113 100644
--- a/cmd/view_block.go
+++ b/cmd/view_block.go
@@ -21,6 +21,7 @@ import (
 	"strconv"
 
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"github.com/spf13/cobra"
 )
@@ -90,4 +91,14 @@ func runViewBlockCmd(cmd *cobra.Command, args []string) {
 	}
 
 	log.Printf("Current Block: %s\n", types.PrettyPrintStruct(block))
+
+	// Print out all balance changes in a given block. This does NOT exempt
+	// any operations/accounts from parsing.
+	parser := parser.New(newFetcher.Asserter, func(*types.Operation) bool { return false })
+	changes, err := parser.BalanceChanges(ctx, block, false)
+	if err != nil {
+		log.Fatal(fmt.Errorf("%w: unable to calculate balance changes", err))
+	}
+
+	log.Printf("Balance Changes: %s\n", types.PrettyPrintStruct(changes))
 }
diff --git a/go.mod b/go.mod
index 46b326b..73f858e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,8 @@ module github.com/coinbase/rosetta-cli
 go 1.13
 
 require (
-	github.com/coinbase/rosetta-sdk-go v0.1.8
+	github.com/coinbase/rosetta-sdk-go v0.1.10
 	github.com/dgraph-io/badger v1.6.0
-	github.com/google/addlicense v0.0.0-20200422172452-68a83edd47bc // indirect
-	github.com/mattn/goveralls v0.0.5 // indirect
-	github.com/segmentio/golines v0.0.0-20200306054842-869934f8da7b // indirect
 	github.com/spf13/cobra v0.0.5
 	github.com/stretchr/testify v1.5.1
 	golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
diff --git a/go.sum b/go.sum
index afacd67..c0817b1 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,10 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/coinbase/rosetta-sdk-go v0.1.8 h1:YJf5Us7Pl4G8CRBZ5+LEUIV4k9ZI/18nFloPaHBySFk=
 github.com/coinbase/rosetta-sdk-go v0.1.8/go.mod h1:y1wXRc1wod4fEp3jhW+9D6MSFtnm/9X5yNxJv92Cv2E=
+github.com/coinbase/rosetta-sdk-go v0.1.9 h1:HW60xBt2h54QL0AlfMpBM05lrwlV4FE/ZnMYF0002Ak=
+github.com/coinbase/rosetta-sdk-go v0.1.9/go.mod h1:Z3yIflVjfPH1tYN/ucYcnJuXnxIr1xzO26YLla6jYLw=
+github.com/coinbase/rosetta-sdk-go v0.1.10 h1:AzmvZXhsTNjXAcLfkgdbO0OWTj++ZeyW4ZXFyd9Gn1w=
+github.com/coinbase/rosetta-sdk-go v0.1.10/go.mod h1:Z3yIflVjfPH1tYN/ucYcnJuXnxIr1xzO26YLla6jYLw=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -92,16 +96,20 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
 github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
 golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -123,10 +131,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03 h1:4gtJXHJ9ud0q8MNSDxJsRU/WH+afypbe4Vk4zq+8qow=
 golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200113040837-eac381796e91 h1:OOkytthzFBKHY5EfEgLUabprb0LtJVkQtNxAQ02+UE4=
 golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c h1:TDspWmUQsjdWzrHnd5imfaJSfhR4AO/R7kG++T2cONw=
+golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
index 2afe02e..248ef25 100644
--- a/internal/logger/logger.go
+++ b/internal/logger/logger.go
@@ -21,8 +21,7 @@ import (
 	"os"
 	"path"
 
-	"github.com/coinbase/rosetta-cli/internal/reconciler"
-
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 )
 
@@ -223,7 +222,7 @@ func (l *Logger) TransactionStream(
 // to the balanceStreamFile.
 func (l *Logger) BalanceStream(
 	ctx context.Context,
-	balanceChanges []*reconciler.BalanceChange,
+	balanceChanges []*parser.BalanceChange,
 ) error {
 	if !l.logBalanceChanges {
 		return nil
diff --git a/internal/processor/storage_helper.go b/internal/processor/storage_helper.go
index ce3d259..9f63710 100644
--- a/internal/processor/storage_helper.go
+++ b/internal/processor/storage_helper.go
@@ -17,11 +17,12 @@ package processor
 import (
 	"context"
 	"fmt"
-	"log"
 
 	"github.com/coinbase/rosetta-cli/internal/reconciler"
 
+	"github.com/coinbase/rosetta-sdk-go/asserter"
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 )
 
@@ -89,50 +90,20 @@ func (h *BlockStorageHelper) AccountBalance(
 	}, nil
 }
 
-// SkipOperation returns a boolean indicating whether
-// an operation should be processed. An operation will
-// not be processed if it is considered unsuccessful
-// or affects an exempt account.
-func (h *BlockStorageHelper) SkipOperation(
-	ctx context.Context,
-	op *types.Operation,
-) (bool, error) {
-	successful, err := h.fetcher.Asserter.OperationSuccessful(op)
-	if err != nil {
-		// Should only occur if responses not validated
-		return false, err
-	}
-
-	if !successful {
-		return true, nil
-	}
-
-	if op.Account == nil {
-		return true, nil
-	}
-
-	// Exempting account in BalanceChanges ensures that storage is not updated
-	// and that the account is not reconciled.
-	if h.accountExempt(op.Account, op.Amount.Currency) {
-		log.Printf("Skipping exempt account %+v\n", op.Account)
-		return true, nil
-	}
-
-	return false, nil
+// Asserter returns a *asserter.Asserter.
+func (h *BlockStorageHelper) Asserter() *asserter.Asserter {
+	return h.fetcher.Asserter
 }
 
-// accountExempt returns a boolean indicating if the provided
-// account and currency are exempt from balance tracking and
-// reconciliation.
-func (h *BlockStorageHelper) accountExempt(
-	account *types.AccountIdentifier,
-	currency *types.Currency,
-) bool {
-	return reconciler.ContainsAccountCurrency(
-		h.exemptAccounts,
-		&reconciler.AccountCurrency{
-			Account:  account,
-			Currency: currency,
-		},
-	)
+// ExemptFunc returns a parser.ExemptOperation.
+func (h *BlockStorageHelper) ExemptFunc() parser.ExemptOperation {
+	return func(op *types.Operation) bool {
+		return reconciler.ContainsAccountCurrency(
+			h.exemptAccounts,
+			&reconciler.AccountCurrency{
+				Account:  op.Account,
+				Currency: op.Amount.Currency,
+			},
+		)
+	}
 }
diff --git a/internal/reconciler/reconciler.go b/internal/reconciler/reconciler.go
index a46d359..d8ffe58 100644
--- a/internal/reconciler/reconciler.go
+++ b/internal/reconciler/reconciler.go
@@ -25,6 +25,7 @@ import (
 	"github.com/coinbase/rosetta-cli/internal/utils"
 
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"golang.org/x/sync/errgroup"
 )
@@ -91,15 +92,6 @@ var (
 	ErrBlockGone = errors.New("block gone")
 )
 
-// BalanceChange represents a balance change that affected
-// a *types.AccountIdentifier and a *types.Currency.
-type BalanceChange struct {
-	Account    *types.AccountIdentifier `json:"account_identifier,omitempty"`
-	Currency   *types.Currency          `json:"currency,omitempty"`
-	Block      *types.BlockIdentifier   `json:"block_identifier,omitempty"`
-	Difference string                   `json:"difference,omitempty"`
-}
-
 // Helper functions are used by Reconciler to compare
 // computed balances from a block with the balance calculated
 // by the node. Defining an interface allows the client to determine
@@ -158,7 +150,7 @@ type Reconciler struct {
 	accountConcurrency   uint64
 	lookupBalanceByBlock bool
 	interestingAccounts  []*AccountCurrency
-	changeQueue          chan *BalanceChange
+	changeQueue          chan *parser.BalanceChange
 
 	// highWaterMark is used to skip requests when
 	// we are very far behind the live head.
@@ -167,7 +159,7 @@ type Reconciler struct {
 	// seenAccts are stored for inactive account
 	// reconciliation.
 	seenAccts     []*AccountCurrency
-	inactiveQueue []*BalanceChange
+	inactiveQueue []*parser.BalanceChange
 
 	// inactiveQueueMutex needed because we can't peek at the tip
 	// of a channel to determine when it is ready to look at.
@@ -195,18 +187,18 @@ func NewReconciler(
 		interestingAccounts:  interestingAccounts,
 		highWaterMark:        -1,
 		seenAccts:            make([]*AccountCurrency, 0),
-		inactiveQueue:        make([]*BalanceChange, 0),
+		inactiveQueue:        make([]*parser.BalanceChange, 0),
 	}
 
 	if lookupBalanceByBlock {
 		// When lookupBalanceByBlock is enabled, we check
 		// balance changes synchronously.
-		r.changeQueue = make(chan *BalanceChange)
+		r.changeQueue = make(chan *parser.BalanceChange)
 	} else {
 		// When lookupBalanceByBlock is disabled, we must check
 		// balance changes asynchronously. Using a buffered
 		// channel allows us to add balance changes without blocking.
-		r.changeQueue = make(chan *BalanceChange, backlogThreshold)
+		r.changeQueue = make(chan *parser.BalanceChange, backlogThreshold)
 	}
 
 	return r
@@ -217,7 +209,7 @@ func NewReconciler(
 func (r *Reconciler) QueueChanges(
 	ctx context.Context,
 	block *types.BlockIdentifier,
-	balanceChanges []*BalanceChange,
+	balanceChanges []*parser.BalanceChange,
 ) error {
 	// Ensure all interestingAccounts are checked
 	for _, account := range r.interestingAccounts {
@@ -236,7 +228,7 @@ func (r *Reconciler) QueueChanges(
 		}
 
 		// If account + currency not found, add with difference 0
-		balanceChanges = append(balanceChanges, &BalanceChange{
+		balanceChanges = append(balanceChanges, &parser.BalanceChange{
 			Account:    account.Account,
 			Currency:   account.Currency,
 			Difference: zeroString,
@@ -492,7 +484,7 @@ func (r *Reconciler) inactiveAccountQueue(
 
 	if inactive || shouldEnqueueInactive {
 		r.inactiveQueueMutex.Lock()
-		r.inactiveQueue = append(r.inactiveQueue, &BalanceChange{
+		r.inactiveQueue = append(r.inactiveQueue, &parser.BalanceChange{
 			Account:  accountCurrency.Account,
 			Currency: accountCurrency.Currency,
 			Block:    liveBlock,
diff --git a/internal/storage/block_storage.go b/internal/storage/block_storage.go
index cac9ac5..8a78f2c 100644
--- a/internal/storage/block_storage.go
+++ b/internal/storage/block_storage.go
@@ -27,9 +27,10 @@ import (
 	"math/big"
 	"path"
 
-	"github.com/coinbase/rosetta-cli/internal/reconciler"
 	"github.com/coinbase/rosetta-cli/internal/syncer"
 
+	"github.com/coinbase/rosetta-sdk-go/asserter"
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 )
 
@@ -136,10 +137,8 @@ type Helper interface {
 		block *types.BlockIdentifier,
 	) (*types.Amount, error)
 
-	SkipOperation(
-		ctx context.Context,
-		op *types.Operation,
-	) (bool, error)
+	ExemptFunc() parser.ExemptOperation
+	Asserter() *asserter.Asserter
 }
 
 // BlockStorage implements block specific storage methods
@@ -147,6 +146,7 @@ type Helper interface {
 type BlockStorage struct {
 	db     Database
 	helper Helper
+	parser *parser.Parser
 }
 
 // NewBlockStorage returns a new BlockStorage.
@@ -158,6 +158,7 @@ func NewBlockStorage(
 	return &BlockStorage{
 		db:     db,
 		helper: helper,
+		parser: parser.New(helper.Asserter(), helper.ExemptFunc()),
 	}
 }
 
@@ -277,7 +278,7 @@ func (b *BlockStorage) storeHash(
 func (b *BlockStorage) StoreBlock(
 	ctx context.Context,
 	block *types.Block,
-) ([]*reconciler.BalanceChange, error) {
+) ([]*parser.BalanceChange, error) {
 	transaction := b.newDatabaseTransaction(ctx, true)
 	defer transaction.Discard(ctx)
 	buf := new(bytes.Buffer)
@@ -306,9 +307,9 @@ func (b *BlockStorage) StoreBlock(
 		}
 	}
 
-	changes, err := b.BalanceChanges(ctx, block, false)
+	changes, err := b.parser.BalanceChanges(ctx, block, false)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%w: unable to calculate balance changes", err)
 	}
 
 	for _, change := range changes {
@@ -335,7 +336,7 @@ func (b *BlockStorage) StoreBlock(
 func (b *BlockStorage) RemoveBlock(
 	ctx context.Context,
 	blockIdentifier *types.BlockIdentifier,
-) ([]*reconciler.BalanceChange, error) {
+) ([]*parser.BalanceChange, error) {
 	block, err := b.GetBlock(ctx, blockIdentifier)
 	if err != nil {
 		return nil, err
@@ -344,9 +345,9 @@ func (b *BlockStorage) RemoveBlock(
 	transaction := b.newDatabaseTransaction(ctx, true)
 	defer transaction.Discard(ctx)
 
-	changes, err := b.BalanceChanges(ctx, block, true)
+	changes, err := b.parser.BalanceChanges(ctx, block, true)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%w: unable to calculate balance changes", err)
 	}
 
 	for _, change := range changes {
@@ -483,7 +484,7 @@ func (b *BlockStorage) SetBalance(
 func (b *BlockStorage) UpdateBalance(
 	ctx context.Context,
 	dbTransaction DatabaseTransaction,
-	change *reconciler.BalanceChange,
+	change *parser.BalanceChange,
 	parentBlock *types.BlockIdentifier,
 ) error {
 	if change.Currency == nil {
@@ -686,77 +687,6 @@ func (b *BlockStorage) BootstrapBalances(
 	return nil
 }
 
-// BalanceChanges returns all balance changes for
-// a particular block. All balance changes for a
-// particular account are summed into a single
-// storage.BalanceChanges struct. If a block is being
-// orphaned, the opposite of each balance change is
-// returned.
-func (b *BlockStorage) BalanceChanges(
-	ctx context.Context,
-	block *types.Block,
-	blockRemoved bool,
-) ([]*reconciler.BalanceChange, error) {
-	balanceChanges := map[string]*reconciler.BalanceChange{}
-	for _, tx := range block.Transactions {
-		for _, op := range tx.Operations {
-			skip, err := b.helper.SkipOperation(
-				ctx,
-				op,
-			)
-			if err != nil {
-				return nil, err
-			}
-			if skip {
-				continue
-			}
-
-			amount := op.Amount
-			blockIdentifier := block.BlockIdentifier
-			if blockRemoved {
-				existing, ok := new(big.Int).SetString(amount.Value, 10)
-				if !ok {
-					return nil, fmt.Errorf("%s is not an integer", amount.Value)
-				}
-
-				amount.Value = new(big.Int).Neg(existing).String()
-				blockIdentifier = block.ParentBlockIdentifier
-			}
-
-			// Merge values by account and currency
-			key := fmt.Sprintf(
-				"%x",
-				GetBalanceKey(op.Account, op.Amount.Currency),
-			)
-
-			val, ok := balanceChanges[key]
-			if !ok {
-				balanceChanges[key] = &reconciler.BalanceChange{
-					Account:    op.Account,
-					Currency:   op.Amount.Currency,
-					Difference: amount.Value,
-					Block:      blockIdentifier,
-				}
-				continue
-			}
-
-			newDifference, err := types.AddValues(val.Difference, amount.Value)
-			if err != nil {
-				return nil, err
-			}
-			val.Difference = newDifference
-			balanceChanges[key] = val
-		}
-	}
-
-	allChanges := []*reconciler.BalanceChange{}
-	for _, change := range balanceChanges {
-		allChanges = append(allChanges, change)
-	}
-
-	return allChanges, nil
-}
-
 // CreateBlockCache populates a slice of blocks with the most recent
 // ones in storage.
 func (b *BlockStorage) CreateBlockCache(ctx context.Context) []*types.BlockIdentifier {
diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go
index f4b4e8f..dd62b97 100644
--- a/internal/syncer/syncer.go
+++ b/internal/syncer/syncer.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"time"
 
 	"github.com/coinbase/rosetta-cli/internal/utils"
 
@@ -40,6 +41,12 @@ const (
 	PastBlockSize = 20
 )
 
+var (
+	// defaultSyncSleep is the amount of time to sleep
+	// when we are at tip but want to keep syncing.
+	defaultSyncSleep = 5 * time.Second
+)
+
 // Handler is called at various times during the sync cycle
 // to handle different events. It is common to write logs or
 // perform reconciliation in the sync processor.
@@ -282,8 +289,15 @@ func (s *Syncer) Sync(
 		if err != nil {
 			return fmt.Errorf("%w: unable to get next syncable range", err)
 		}
+
 		if halt {
-			break
+			if endIndex != -1 {
+				break
+			}
+
+			log.Printf("Syncer at tip %d...sleeping\n", s.nextIndex)
+			time.Sleep(defaultSyncSleep)
+			continue
 		}
 
 		log.Printf("Syncing %d-%d\n", s.nextIndex, rangeEnd)
@@ -298,6 +312,10 @@ func (s *Syncer) Sync(
 		}
 	}
 
+	if startIndex == -1 {
+		startIndex = s.genesisBlock.Index
+	}
+
 	log.Printf("Finished syncing %d-%d\n", startIndex, endIndex)
 	return nil
 }


Test patch:
diff --git a/internal/storage/block_storage_test.go b/internal/storage/block_storage_test.go
index 6e757ab..302a032 100644
--- a/internal/storage/block_storage_test.go
+++ b/internal/storage/block_storage_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/coinbase/rosetta-cli/internal/utils"
 
 	"github.com/coinbase/rosetta-sdk-go/asserter"
+	"github.com/coinbase/rosetta-sdk-go/parser"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"github.com/stretchr/testify/assert"
 )
@@ -99,6 +100,35 @@ func TestHeadBlockIdentifier(t *testing.T) {
 	})
 }
 
+func simpleTransactionFactory(
+	hash string,
+	address string,
+	value string,
+	currency *types.Currency,
+) *types.Transaction {
+	return &types.Transaction{
+		TransactionIdentifier: &types.TransactionIdentifier{
+			Hash: hash,
+		},
+		Operations: []*types.Operation{
+			{
+				OperationIdentifier: &types.OperationIdentifier{
+					Index: 0,
+				},
+				Type:   "Transfer",
+				Status: "Success",
+				Account: &types.AccountIdentifier{
+					Address: address,
+				},
+				Amount: &types.Amount{
+					Value:    value,
+					Currency: currency,
+				},
+			},
+		},
+	}
+}
+
 var (
 	newBlock = &types.Block{
 		BlockIdentifier: &types.BlockIdentifier{
@@ -111,18 +141,7 @@ var (
 		},
 		Timestamp: 1,
 		Transactions: []*types.Transaction{
-			{
-				TransactionIdentifier: &types.TransactionIdentifier{
-					Hash: "blahTx",
-				},
-				Operations: []*types.Operation{
-					{
-						OperationIdentifier: &types.OperationIdentifier{
-							Index: 0,
-						},
-					},
-				},
-			},
+			simpleTransactionFactory("blahTx", "addr1", "100", &types.Currency{Symbol: "hello"}),
 		},
 	}
 
@@ -142,18 +161,7 @@ var (
 		},
 		Timestamp: 1,
 		Transactions: []*types.Transaction{
-			{
-				TransactionIdentifier: &types.TransactionIdentifier{
-					Hash: "blahTx",
-				},
-				Operations: []*types.Operation{
-					{
-						OperationIdentifier: &types.OperationIdentifier{
-							Index: 0,
-						},
-					},
-				},
-			},
+			simpleTransactionFactory("blahTx", "addr1", "100", &types.Currency{Symbol: "hello"}),
 		},
 	}
 
@@ -365,7 +373,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account,
 				Currency:   currency,
 				Block:      newBlock,
@@ -388,7 +396,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account3,
 				Currency:   currency,
 				Block:      newBlock,
@@ -412,7 +420,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account,
 				Currency:   nil,
 				Block:      newBlock,
@@ -434,7 +442,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account,
 				Currency:   currency,
 				Block:      newBlock2,
@@ -456,7 +464,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account,
 				Currency:   currency,
 				Block:      newBlock3,
@@ -480,7 +488,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account,
 				Currency:   largeDeduction.Currency,
 				Block:      newBlock3,
@@ -497,7 +505,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    account2,
 				Currency:   largeDeduction.Currency,
 				Block:      newBlock2,
@@ -515,7 +523,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    subAccount,
 				Currency:   amount.Currency,
 				Block:      newBlock,
@@ -542,7 +550,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    subAccountMetadata,
 				Currency:   amount.Currency,
 				Block:      newBlock,
@@ -569,7 +577,7 @@ func TestBalance(t *testing.T) {
 		err := storage.UpdateBalance(
 			ctx,
 			txn,
-			&reconciler.BalanceChange{
+			&parser.BalanceChange{
 				Account:    subAccountMetadata2,
 				Currency:   amount.Currency,
 				Block:      newBlock,
@@ -748,261 +756,6 @@ func TestBootstrapBalances(t *testing.T) {
 	})
 }
 
-func simpleTransactionFactory(
-	hash string,
-	address string,
-	value string,
-	currency *types.Currency,
-) *types.Transaction {
-	return &types.Transaction{
-		TransactionIdentifier: &types.TransactionIdentifier{
-			Hash: hash,
-		},
-		Operations: []*types.Operation{
-			{
-				OperationIdentifier: &types.OperationIdentifier{
-					Index: 0,
-				},
-				Type:   "Transfer",
-				Status: "Success",
-				Account: &types.AccountIdentifier{
-					Address: address,
-				},
-				Amount: &types.Amount{
-					Value:    value,
-					Currency: currency,
-				},
-			},
-		},
-	}
-}
-
-func TestBalanceChanges(t *testing.T) {
-	var (
-		currency = &types.Currency{
-			Symbol:   "Blah",
-			Decimals: 2,
-		}
-
-		recipient = &types.AccountIdentifier{
-			Address: "acct1",
-		}
-
-		recipientAmount = &types.Amount{
-			Value:    "100",
-			Currency: currency,
-		}
-
-		recipientOperation = &types.Operation{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 0,
-			},
-			Type:    "Transfer",
-			Status:  "Success",
-			Account: recipient,
-			Amount:  recipientAmount,
-		}
-
-		recipientFailureOperation = &types.Operation{
-			OperationIdentifier: &types.OperationIdentifier{
-				Index: 1,
-			},
-			Type:    "Transfer",
-			Status:  "Failure",
-			Account: recipient,
-			Amount:  recipientAmount,
-		}
-
-		recipientTransaction = &types.Transaction{
-			TransactionIdentifier: &types.TransactionIdentifier{
-				Hash: "tx1",
-			},
-			Operations: []*types.Operation{
-				recipientOperation,
-				recipientFailureOperation,
-			},
-		}
-	)
-
-	var tests = map[string]struct {
-		block          *types.Block
-		orphan         bool
-		changes        []*reconciler.BalanceChange
-		exemptAccounts []*reconciler.AccountCurrency
-		err            error
-	}{
-		"simple block": {
-			block: &types.Block{
-				BlockIdentifier: &types.BlockIdentifier{
-					Hash:  "1",
-					Index: 1,
-				},
-				ParentBlockIdentifier: &types.BlockIdentifier{
-					Hash:  "0",
-					Index: 0,
-				},
-				Transactions: []*types.Transaction{
-					recipientTransaction,
-				},
-				Timestamp: asserter.MinUnixEpoch + 1,
-			},
-			orphan: false,
-			changes: []*reconciler.BalanceChange{
-				{
-					Account:  recipient,
-					Currency: currency,
-					Block: &types.BlockIdentifier{
-						Hash:  "1",
-						Index: 1,
-					},
-					Difference: "100",
-				},
-			},
-			err: nil,
-		},
-		"simple block account exempt": {
-			block: &types.Block{
-				BlockIdentifier: &types.BlockIdentifier{
-					Hash:  "1",
-					Index: 1,
-				},
-				ParentBlockIdentifier: &types.BlockIdentifier{
-					Hash:  "0",
-					Index: 0,
-				},
-				Transactions: []*types.Transaction{
-					recipientTransaction,
-				},
-				Timestamp: asserter.MinUnixEpoch + 1,
-			},
-			orphan:  false,
-			changes: []*reconciler.BalanceChange{},
-			exemptAccounts: []*reconciler.AccountCurrency{
-				{
-					Account:  recipient,
-					Currency: currency,
-				},
-			},
-			err: nil,
-		},
-		"single account sum block": {
-			block: &types.Block{
-				BlockIdentifier: &types.BlockIdentifier{
-					Hash:  "1",
-					Index: 1,
-				},
-				ParentBlockIdentifier: &types.BlockIdentifier{
-					Hash:  "0",
-					Index: 0,
-				},
-				Transactions: []*types.Transaction{
-					simpleTransactionFactory("tx1", "addr1", "100", currency),
-					simpleTransactionFactory("tx2", "addr1", "150", currency),
-					simpleTransactionFactory("tx3", "addr2", "150", currency),
-				},
-				Timestamp: asserter.MinUnixEpoch + 1,
-			},
-			orphan: false,
-			changes: []*reconciler.BalanceChange{
-				{
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Currency: currency,
-					Block: &types.BlockIdentifier{
-						Hash:  "1",
-						Index: 1,
-					},
-					Difference: "250",
-				},
-				{
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Currency: currency,
-					Block: &types.BlockIdentifier{
-						Hash:  "1",
-						Index: 1,
-					},
-					Difference: "150",
-				},
-			},
-			err: nil,
-		},
-		"single account sum orphan block": {
-			block: &types.Block{
-				BlockIdentifier: &types.BlockIdentifier{
-					Hash:  "1",
-					Index: 1,
-				},
-				ParentBlockIdentifier: &types.BlockIdentifier{
-					Hash:  "0",
-					Index: 0,
-				},
-				Transactions: []*types.Transaction{
-					simpleTransactionFactory("tx1", "addr1", "100", currency),
-					simpleTransactionFactory("tx2", "addr1", "150", currency),
-					simpleTransactionFactory("tx3", "addr2", "150", currency),
-				},
-				Timestamp: asserter.MinUnixEpoch + 1,
-			},
-			orphan: true,
-			changes: []*reconciler.BalanceChange{
-				{
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Currency: currency,
-					Block: &types.BlockIdentifier{
-						Hash:  "0",
-						Index: 0,
-					},
-					Difference: "-250",
-				},
-				{
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Currency: currency,
-					Block: &types.BlockIdentifier{
-						Hash:  "0",
-						Index: 0,
-					},
-					Difference: "-150",
-				},
-			},
-			err: nil,
-		},
-	}
-
-	ctx := context.Background()
-
-	newDir, err := utils.CreateTempDir()
-	assert.NoError(t, err)
-	defer utils.RemoveTempDir(newDir)
-
-	database, err := NewBadgerStorage(ctx, newDir)
-	assert.NoError(t, err)
-	defer database.Close(ctx)
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			storage := NewBlockStorage(ctx, database, &MockBlockStorageHelper{
-				ExemptAccounts: test.exemptAccounts,
-			})
-
-			changes, err := storage.BalanceChanges(
-				ctx,
-				test.block,
-				test.orphan,
-			)
-
-			assert.ElementsMatch(t, test.changes, changes)
-			assert.Equal(t, test.err, err)
-		})
-	}
-}
-
 func TestCreateBlockCache(t *testing.T) {
 	ctx := context.Background()
 
@@ -1063,24 +816,33 @@ func (h *MockBlockStorageHelper) AccountBalance(
 	}, nil
 }
 
-func (h *MockBlockStorageHelper) SkipOperation(
-	ctx context.Context,
-	op *types.Operation,
-) (bool, error) {
-	if op.Account == nil || op.Amount == nil {
-		return true, nil
-	}
-
-	if op.Status == "Failure" {
-		return true, nil
-	}
+func (h *MockBlockStorageHelper) Asserter() *asserter.Asserter {
+	a, _ := asserter.NewClientWithOptions(
+		&types.NetworkIdentifier{
+			Blockchain: "bitcoin",
+			Network:    "mainnet",
+		},
+		&types.BlockIdentifier{
+			Hash:  "block 0",
+			Index: 0,
+		},
+		[]string{"Transfer"},
+		[]*types.OperationStatus{
+			{
+				Status:     "Success",
+				Successful: true,
+			},
+		},
+		[]*types.Error{},
+	)
+	return a
+}
 
-	if reconciler.ContainsAccountCurrency(h.ExemptAccounts, &reconciler.AccountCurrency{
-		Account:  op.Account,
-		Currency: op.Amount.Currency,
-	}) {
-		return true, nil
+func (h *MockBlockStorageHelper) ExemptFunc() parser.ExemptOperation {
+	return func(op *types.Operation) bool {
+		return reconciler.ContainsAccountCurrency(h.ExemptAccounts, &reconciler.AccountCurrency{
+			Account:  op.Account,
+			Currency: op.Amount.Currency,
+		})
 	}
-
-	return false, nil
 }