Repository: algorand/indexer
Pull request #512 (2021-05-25 05:16:47)

Move encoding/decoding functions for postgres to a separate package.
## Summary

Move encoding/decoding functions used for writing/reading from DB to a separate package for better modularity. Also, code that encodes transactions is simplified. `JSONOneLine` now has a duplicate in `util/` directory, but I think it ok to have two: one is DB internal, the other is for miscellaneous needs.

## Test Plan

Using existing tests.


Patch:
diff --git a/cmd/block-generator/generator/generate.go b/cmd/block-generator/generator/generate.go
index 430d223..881963f 100644
--- a/cmd/block-generator/generator/generate.go
+++ b/cmd/block-generator/generator/generate.go
@@ -9,9 +9,30 @@ import (
 
 	"github.com/algorand/go-algorand-sdk/encoding/json"
 	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
+	"github.com/algorand/go-algorand-sdk/future"
 	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util"
+)
+
+var errOutOfRange = fmt.Errorf("selection is out of weighted range")
+
+const (
+	// Generator types
+	paymentTx = "pay"
+	assetTx   = "acfg"
+	//keyRegistrationTx = "keyreg"
+	//applicationCallTx = "appl"
+
+	// Asset Tx Types
+	assetCreate  = "create"
+	assetOptin   = "optin"
+	assetXfer    = "xfer"
+	assetClose   = "close"
+	assetDestroy = "destroy"
+
+	assetTotal = uint64(100000000000000000)
 )
 
 const (
@@ -61,7 +82,7 @@ func MakeGenerator(config GenerationConfig) (Generator, error) {
 		return nil, fmt.Errorf("payment configuration ratios should equal 1")
 	}
 
-	if !sumIsCloseToOne(config.AssetCreateFraction, config.AssetOptinFraction, config.AssetCloseFraction, config.AssetXferFraction) {
+	if !sumIsCloseToOne(config.AssetCreateFraction, config.AssetDestroyFraction, config.AssetOptinFraction, config.AssetCloseFraction, config.AssetXferFraction) {
 		return nil, fmt.Errorf("asset configuration ratios should equal 1")
 	}
 
@@ -85,6 +106,30 @@ func MakeGenerator(config GenerationConfig) (Generator, error) {
 
 	gen.initializeAccounting()
 
+	for _, val := range getTransactionOptions() {
+		switch val {
+		case paymentTx:
+			gen.transactionWeights = append(gen.transactionWeights, config.PaymentTransactionFraction)
+		case assetTx:
+			gen.transactionWeights = append(gen.transactionWeights, config.AssetTransactionFraction)
+		}
+	}
+
+	for _, val := range getAssetTxOptions() {
+		switch val {
+		case assetCreate:
+			gen.assetTxWeights = append(gen.assetTxWeights, config.AssetCreateFraction)
+		case assetDestroy:
+			gen.assetTxWeights = append(gen.assetTxWeights, config.AssetDestroyFraction)
+		case assetOptin:
+			gen.assetTxWeights = append(gen.assetTxWeights, config.AssetOptinFraction)
+		case assetXfer:
+			gen.assetTxWeights = append(gen.assetTxWeights, config.AssetXferFraction)
+		case assetClose:
+			gen.assetTxWeights = append(gen.assetTxWeights, config.AssetCloseFraction)
+		}
+	}
+
 	return gen, nil
 }
 
@@ -120,12 +165,30 @@ type generator struct {
 	rewardsRate               uint64
 	rewardsRecalculationRound uint64
 
-	// balances To avoid crypto and reduce storage, accounts are faked.
+	// balances for all accounts. 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
+	// assets is a minimal representation of the asset holdings, it doesn't
+	// include the frozen state.
+	assets []*assetData
+
+	transactionWeights []float32
+	assetTxWeights     []float32
+}
+
+type assetData struct {
+	assetID uint64
+	creator uint64
+	// Holding at index 0 is the creator.
+	holdings []assetHolding
+	// Set of holders in the holdings array for easy reference.
+	holders map[uint64]bool
+}
+
+type assetHolding struct {
+	acctIndex uint64
+	balance   uint64
 }
 
 func (g *generator) WriteGenesis(output io.Writer) {
@@ -154,9 +217,24 @@ func (g *generator) WriteGenesis(output io.Writer) {
 	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)
+func getTransactionOptions() []interface{} {
+	return []interface{}{paymentTx, assetTx}
+}
+
+func (g *generator) generateTransaction(sp sdk_types.SuggestedParams, round uint64, intra uint64) (types.SignedTxnInBlock, error) {
+	selection, err := weightedSelection(g.transactionWeights, getTransactionOptions(), paymentTx)
+	if err != nil {
+		return types.SignedTxnInBlock{}, err
+	}
+
+	switch selection {
+	case paymentTx:
+		return g.generatePaymentTxn(sp, round, intra)
+	case assetTx:
+		return g.generateAssetTxn(sp, round, intra)
+	default:
+		return types.SignedTxnInBlock{}, fmt.Errorf("no generator available for %s", selection)
+	}
 }
 
 // WriteBlock generates a block full of new transactions and writes it to the writer.
@@ -166,20 +244,18 @@ func (g *generator) WriteBlock(output io.Writer, round uint64) {
 	}
 
 	// Generate the transactions
+	sp := g.getSuggestedParams(round)
 	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,
-		}
+		txn, err := g.generateTransaction(sp, g.round, i)
 		if err != nil {
 			panic(fmt.Sprintf("failed to generate transaction: %v\n", err))
 		}
-		transactions = append(transactions, stxnib)
+		transactions = append(transactions, txn)
 	}
 
+	g.txnCounter += g.config.TxnPerBlock
+
 	block := types.Block{
 		BlockHeader: types.BlockHeader{
 			Round:       types.Round(g.round),
@@ -212,7 +288,6 @@ func (g *generator) WriteBlock(output io.Writer, round uint64) {
 		Certificate: types.Certificate{},
 	}
 
-	g.txnCounter += g.config.TxnPerBlock
 	g.timestamp += consensusTimeMilli
 	g.round++
 
@@ -239,7 +314,7 @@ func (g *generator) initializeAccounting() {
 }
 
 // convert wraps the transaction as needed for including in the block.
-func convert(txn sdk_types.Transaction, ad types.ApplyData) types.SignedTxnWithAD {
+func convert(txn sdk_types.Transaction, ad types.ApplyData) types.SignedTxnInBlock {
 	stxn := sdk_types.SignedTxn{
 		Sig:      sdk_types.Signature{},
 		Msig:     sdk_types.MultisigSig{},
@@ -257,12 +332,30 @@ func convert(txn sdk_types.Transaction, ad types.ApplyData) types.SignedTxnWithA
 		ApplyData: ad,
 	}
 
-	return withAd
+	stxnib := types.SignedTxnInBlock{
+		SignedTxnWithAD: withAd,
+		HasGenesisID:    true,
+		HasGenesisHash:  true,
+	}
+	return stxnib
+}
+
+func (g *generator) getSuggestedParams(round uint64) sdk_types.SuggestedParams {
+	return sdk_types.SuggestedParams{
+		Fee:              1000,
+		GenesisID:        g.genesisID,
+		GenesisHash:      g.genesisHash[:],
+		FirstRoundValid:  sdk_types.Round(round),
+		LastRoundValid:   sdk_types.Round(round + 1000),
+		ConsensusVersion: "",
+		FlatFee:          true,
+		MinFee:           1000,
+	}
 }
 
 // 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) {
+func (g *generator) generatePaymentTxn(sp sdk_types.SuggestedParams, _ uint64 /*round*/, _ uint64 /*intra*/) (types.SignedTxnInBlock, error) {
 	var receiveIndex uint64
 	if g.numPayments%uint64(100*g.config.PaymentNewAccountFraction) == 0 {
 		g.balances = append(g.balances, 0)
@@ -278,47 +371,201 @@ func (g *generator) generatePaymentTxn(round uint64, intra uint64) (types.Signed
 	sender := indexToAccount(sendIndex)
 	receiver := indexToAccount(receiveIndex)
 
-	amount := uint64(10000000)
+	amount := uint64(1000)
 	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))
+	if g.balances[sendIndex] < (amount + fee) {
+		fmt.Printf(fmt.Sprintf("\n\nthe sender account does not have 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[sendIndex] -= amount + 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{},
-	}
 
+	// TODO: Unnecessary serialize/deserialize of addresses.
+	// Easy to optimize by creating the struct ourselves if needed.
+	txn, err := future.MakePaymentTxn(sender.String(), receiver.String(), amount, nil, "", sp)
+	util.MaybeFail(err, "unable to make transaction")
 	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
+func getAssetTxOptions() []interface{} {
+	return []interface{}{assetCreate, assetDestroy, assetOptin, assetXfer, assetClose}
+}
+
+func (g *generator) generateAssetTxnInternal(txType interface{}, sp sdk_types.SuggestedParams, intra uint64) (txn types.Transaction) {
+	return g.generateAssetTxnInternalHint(txType, sp, intra, 0, nil)
+}
+
+func (g *generator) generateAssetTxnInternalHint(txType interface{}, sp sdk_types.SuggestedParams, intra uint64, hintIndex uint64, hint *assetData) (txn types.Transaction) {
+	// If there are no assets the next operation needs to be a create.
+	if len(g.assets) == 0 {
+		txType = assetCreate
+	}
+
+	var err error
+	numAssets := uint64(len(g.assets))
+	var senderIndex uint64
+
+	if txType == assetCreate {
+		senderIndex = numAssets % g.numAccounts
+		senderAcct := indexToAccount(senderIndex)
+		senderAcctStr := senderAcct.String()
+
+		total := assetTotal
+		assetID := g.txnCounter + intra + 1
+		assetName := fmt.Sprintf("asset #%d", assetID)
+		txn, err = future.MakeAssetCreateTxn(senderAcctStr, nil, sp, total, 0, false, senderAcctStr, senderAcctStr, senderAcctStr, senderAcctStr, "tokens", assetName, "https://algorand.com", "metadata!")
+		util.MaybeFail(err, "unable to make asset create transaction")
+
+		// Compute asset ID and initialize holdings
+		a := assetData{
+			assetID:  assetID,
+			creator:  senderIndex,
+			holdings: []assetHolding{{acctIndex: senderIndex, balance: total}},
+			holders:  map[uint64]bool{senderIndex: true},
+		}
+
+		g.assets = append(g.assets, &a)
+	} else {
+		assetIndex := rand.Uint64() % numAssets
+		asset := g.assets[assetIndex]
+		if hint != nil {
+			assetIndex = hintIndex
+			asset = hint
+		}
+
+		switch txType {
+		case assetDestroy:
+			// delete asset
+
+			// If the creator doesn't have all of them, close instead
+			if asset.holdings[0].balance != assetTotal {
+				return g.generateAssetTxnInternalHint(assetClose, sp, intra, assetIndex, asset)
+			}
+
+			senderIndex = asset.creator
+			creator := indexToAccount(senderIndex)
+			creatorString := creator.String()
+
+			txn, err = future.MakeAssetDestroyTxn(creatorString, nil, sp, asset.assetID)
+			util.MaybeFail(err, "unable to make asset destroy transaction")
+
+			// Remove asset by moving the last element to the deleted index then trimming the slice.
+			g.assets[assetIndex] = g.assets[numAssets-1]
+			g.assets = g.assets[:numAssets-1]
+		case assetOptin:
+			// select a random account from asset to optin
+
+			// If every account holds the asset, close instead of optin
+			if uint64(len(asset.holdings)) == g.numAccounts {
+				return g.generateAssetTxnInternalHint(assetClose, sp, intra, assetIndex, asset)
+			}
+
+			// look for an account that does not hold the asset
+			exists := true
+			for exists {
+				senderIndex = rand.Uint64() % g.numAccounts
+				exists = asset.holders[senderIndex]
+			}
+			account := indexToAccount(senderIndex)
+			accountString := account.String()
+
+			txn, err = future.MakeAssetAcceptanceTxn(accountString, nil, sp, asset.assetID)
+			util.MaybeFail(err, "unable to make asset destroy transaction")
+
+			asset.holdings = append(asset.holdings, assetHolding{
+				acctIndex: senderIndex,
+				balance:   0,
+			})
+			asset.holders[senderIndex] = true
+		case assetXfer:
+			// send from creator (holder[0]) to another random holder (same address is valid)
+
+			// If there aren't enough assets to close one, optin an account instead
+			if len(asset.holdings) == 1 {
+				return g.generateAssetTxnInternalHint(assetOptin, sp, intra, assetIndex, asset)
+			}
+
+			senderIndex = asset.holdings[0].acctIndex
+			senderString := indexToAccount(senderIndex).String()
+
+			receiverIndex := (rand.Uint64() % (uint64(len(asset.holdings)) - uint64(1))) + uint64(1)
+			receiver := indexToAccount(asset.holdings[receiverIndex].acctIndex)
+			receiverString := receiver.String()
+
+			amount := uint64(10)
+
+			txn, err = future.MakeAssetTransferTxn(senderString, receiverString, amount, nil, sp, "", asset.assetID)
+			util.MaybeFail(err, "unable to make asset destroy transaction")
+
+			if asset.holdings[0].balance < amount {
+				fmt.Printf(fmt.Sprintf("\n\ncreator doesn't have enough funds for asset %d\n\n", asset.assetID))
+				os.Exit(1)
+			}
+			if g.balances[asset.holdings[0].acctIndex] < uint64(sp.Fee) {
+				fmt.Printf(fmt.Sprintf("\n\ncreator doesn't have enough funds for transaction %d\n\n", asset.assetID))
+				os.Exit(1)
+			}
+
+			asset.holdings[0].balance -= amount
+			asset.holdings[receiverIndex].balance += amount
+		case assetClose:
+			// select a holder of a random asset to close out
+			// If there aren't enough assets to close one, optin an account instead
+			if len(asset.holdings) == 1 {
+				return g.generateAssetTxnInternalHint(assetOptin, sp, intra, assetIndex, asset)
+			}
+
+			numHoldings := uint64(len(asset.holdings))
+			closeIndex := (rand.Uint64() % (numHoldings - 1)) + uint64(1)
+			senderIndex = asset.holdings[closeIndex].acctIndex
+			senderString := indexToAccount(senderIndex).String()
+
+			closeToAcctIndex := asset.holdings[0].acctIndex
+			closeToAcct := indexToAccount(closeToAcctIndex)
+			closeToAcctString := closeToAcct.String()
+
+			txn, err = future.MakeAssetTransferTxn(senderString, closeToAcctString, 0, nil, sp, closeToAcctString, asset.assetID)
+			util.MaybeFail(err, "unable to make asset destroy transaction")
+
+			asset.holdings[0].balance += asset.holdings[closeIndex].balance
+
+			// Remove asset by moving the last element to the deleted index then trimming the slice.
+			asset.holdings[closeIndex] = asset.holdings[numHoldings-1]
+			asset.holdings = asset.holdings[:numHoldings-1]
+			delete(asset.holders, senderIndex)
+		default:
+		}
+	}
+
+	if indexToAccount(senderIndex) != txn.Sender {
+		fmt.Printf("failed to properly set sender index.")
+		os.Exit(1)
+	}
+
+	txn.Fee = 1000
+	if g.balances[senderIndex] < uint64(txn.Fee) {
+		fmt.Printf(fmt.Sprintf("\n\nthe sender account does not have enough algos for the transfer. idx %d, payment number %d\n\n", senderIndex, g.numPayments))
+		os.Exit(1)
+	}
+	g.balances[senderIndex] -= uint64(txn.Fee)
+	return
+}
+
+func (g *generator) generateAssetTxn(sp sdk_types.SuggestedParams, _ uint64 /*round*/, intra uint64) (types.SignedTxnInBlock, error) {
+	selection, err := weightedSelection(g.assetTxWeights, getAssetTxOptions(), assetXfer)
+	if err != nil {
+		return types.SignedTxnInBlock{}, err
+	}
+
+	txn := g.generateAssetTxnInternal(selection, sp, intra)
+
+	if txn.Type == "" {
+		fmt.Println("Empty asset transaction.")
+		os.Exit(1)
+	}
+
+	return convert(txn, types.ApplyData{}), nil
 }
diff --git a/cmd/block-generator/generator/utils.go b/cmd/block-generator/generator/utils.go
new file mode 100644
index 0000000..fed2c1e
--- /dev/null
+++ b/cmd/block-generator/generator/utils.go
@@ -0,0 +1,29 @@
+package generator
+
+import (
+	"fmt"
+	"math/rand"
+)
+
+func weightedSelection(weights []float32, options []interface{}, defaultOption interface{}) (selection interface{}, err error) {
+	return weightedSelectionInternal(rand.Float32(), weights, options, defaultOption)
+}
+
+func weightedSelectionInternal(selectionNumber float32, weights []float32, options []interface{}, defaultOption interface{}) (selection interface{}, err error) {
+	if len(weights) != len(options) {
+		err = fmt.Errorf("number of weights must equal number of options: %d != %d", len(weights), len(options))
+		return
+	}
+
+	total := float32(0)
+	for i := 0; i < len(weights); i++ {
+		if selectionNumber-total < weights[i] {
+			selection = options[i]
+			return
+		}
+		total += weights[i]
+	}
+
+	selection = defaultOption
+	return
+}
diff --git a/cmd/block-generator/run_generator.sh b/cmd/block-generator/run_generator.sh
index 6eccee2..02c8d6e 100755
--- a/cmd/block-generator/run_generator.sh
+++ b/cmd/block-generator/run_generator.sh
@@ -5,6 +5,8 @@
 POSTGRES_CONTAINER=generator-test-container
 POSTGRES_PORT=15432
 POSTGRES_DATABASE=generator_db
+CONFIG=${1:-"$(dirname $0)/test_config.yml"}
+echo "Using config file: $CONFIG"
 
 function start_postgres() {
   docker rm -f $POSTGRES_CONTAINER > /dev/null 2>&1 || true
@@ -18,7 +20,7 @@ function start_postgres() {
      -e PGPASSWORD=algorand \
      -p $POSTGRES_PORT:5432 \
      postgres
- 
+
    sleep 5
 
   docker exec -it $POSTGRES_CONTAINER psql -Ualgorand -c "create database $POSTGRES_DATABASE"
@@ -33,16 +35,17 @@ trap shutdown EXIT
 
 echo "Building generator."
 go build
-pushd ../.. > /dev/null
+pushd $(dirname "$0")/../.. > /dev/null
 echo "Building indexer."
 make
+popd
 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 &
+$(dirname "$0")/block-generator -port 11111 -config "${CONFIG}" &
 GENERATOR_PID=$!
 echo "Starting indexer"
-./cmd/algorand-indexer/algorand-indexer daemon \
+$(dirname "$0")/../../cmd/algorand-indexer/algorand-indexer daemon \
               -S localhost:8980 \
               --algod-net localhost:11111 \
               --algod-token security-is-our-number-one-priority \
diff --git a/cmd/block-generator/scenarios/config.asset.close.yml b/cmd/block-generator/scenarios/config.asset.close.yml
index 6dfeb3d..5ff60c8 100644
--- a/cmd/block-generator/scenarios/config.asset.close.yml
+++ b/cmd/block-generator/scenarios/config.asset.close.yml
@@ -1,5 +1,5 @@
 name: "Asset Close"
-genesis_acocunts: 1000000
+genesis_accounts: 10000
 genesis_account_balance: 100000000
 tx_per_block: 5000
 
@@ -11,3 +11,6 @@ asset_create_fraction: 0.001
 asset_optin_fraction: 0.50
 asset_close_fraction: 0.40
 asset_xfer_fraction: 0.099
+
+# pay config must equal 1.0
+pay_xfer_fraction: 1.0
diff --git a/cmd/block-generator/scenarios/config.asset.destroy.yml b/cmd/block-generator/scenarios/config.asset.destroy.yml
index ca8fed5..cea7297 100644
--- a/cmd/block-generator/scenarios/config.asset.destroy.yml
+++ b/cmd/block-generator/scenarios/config.asset.destroy.yml
@@ -1,5 +1,5 @@
 name: "Asset Destroy"
-genesis_acocunts: 1000000
+genesis_accounts: 10000
 genesis_account_balance: 100000000
 tx_per_block: 5000
 
@@ -11,3 +11,6 @@ asset_create_fraction: 0.8
 asset_destroy_fraction: 0.2
 asset_optin_fraction: 0.0
 asset_close_fraction: 0.0
+
+# pay config must equal 1.0
+pay_xfer_fraction: 1.0
diff --git a/cmd/block-generator/scenarios/config.asset.xfer.yml b/cmd/block-generator/scenarios/config.asset.xfer.yml
index 5e2d759..cc417a4 100644
--- a/cmd/block-generator/scenarios/config.asset.xfer.yml
+++ b/cmd/block-generator/scenarios/config.asset.xfer.yml
@@ -1,5 +1,5 @@
 name: "Asset Xfer"
-genesis_acocunts: 1000000
+genesis_accounts: 10000
 genesis_account_balance: 100000000
 tx_per_block: 5000
 
@@ -10,3 +10,6 @@ tx_asset_fraction: 1.0
 asset_create_fraction: 0.001
 asset_optin_fraction: 0.10
 asset_xfer_fraction: 0.899
+
+# pay config must equal 1.0
+pay_xfer_fraction: 1.0
diff --git a/cmd/block-generator/scenarios/config.mixed.jumbo.yml b/cmd/block-generator/scenarios/config.mixed.jumbo.yml
index 31c25a3..511d5dd 100644
--- a/cmd/block-generator/scenarios/config.mixed.jumbo.yml
+++ b/cmd/block-generator/scenarios/config.mixed.jumbo.yml
@@ -1,7 +1,7 @@
 name: "Mixed (jumbo)"
-genesis_acocunts: 1000000
+genesis_accounts: 10000
 genesis_account_balance: 100000000
-tx_per_block: 50000
+tx_per_block: 19999
 
 # transaction distribution
 tx_pay_fraction: 0.3
diff --git a/cmd/block-generator/scenarios/config.mixed.yml b/cmd/block-generator/scenarios/config.mixed.yml
index bb892da..2022862 100644
--- a/cmd/block-generator/scenarios/config.mixed.yml
+++ b/cmd/block-generator/scenarios/config.mixed.yml
@@ -1,5 +1,5 @@
 name: "Mixed"
-genesis_acocunts: 1000000
+genesis_accounts: 10000
 genesis_account_balance: 100000000
 tx_per_block: 5000
 
diff --git a/cmd/block-generator/scenarios/config.payment.full.yml b/cmd/block-generator/scenarios/config.payment.full.yml
index 22c96fc..c9869ba 100644
--- a/cmd/block-generator/scenarios/config.payment.full.yml
+++ b/cmd/block-generator/scenarios/config.payment.full.yml
@@ -1,5 +1,5 @@
 name: "Pay (full)"
-genesis_acocunts: 10
+genesis_accounts: 10
 genesis_account_balance: 1000000000
 tx_per_block: 5000
 
@@ -9,3 +9,6 @@ tx_pay_fraction: 1.0
 # payment config
 pay_acct_create_fraction: 0.02
 pay_xfer_fraction: 0.98
+
+# asset config must be 1.0
+asset_create_fraction: 1.0
diff --git a/cmd/block-generator/scenarios/config.payment.jumbo.yml b/cmd/block-generator/scenarios/config.payment.jumbo.yml
index 2294b46..60fe40d 100644
--- a/cmd/block-generator/scenarios/config.payment.jumbo.yml
+++ b/cmd/block-generator/scenarios/config.payment.jumbo.yml
@@ -1,7 +1,7 @@
 name: "Pay (jumbo)"
-genesis_acocunts: 10
+genesis_accounts: 10
 genesis_account_balance: 1000000000
-tx_per_block: 50000
+tx_per_block: 19999
 
 # transaction distribution
 tx_pay_fraction: 1.0
@@ -9,3 +9,6 @@ tx_pay_fraction: 1.0
 # payment config
 pay_acct_create_fraction: 0.02
 pay_xfer_fraction: 0.98
+
+# asset config must be 1.0
+asset_create_fraction: 1.0
diff --git a/cmd/block-generator/scenarios/config.payment.small.yml b/cmd/block-generator/scenarios/config.payment.small.yml
index 4dcbb64..fef73da 100644
--- a/cmd/block-generator/scenarios/config.payment.small.yml
+++ b/cmd/block-generator/scenarios/config.payment.small.yml
@@ -1,5 +1,5 @@
 name: "Pay (small)"
-genesis_acocunts: 10
+genesis_accounts: 10
 genesis_account_balance: 1000000000
 tx_per_block: 100
 
@@ -9,3 +9,6 @@ tx_pay_fraction: 1.0
 # payment config
 pay_acct_create_fraction: 0.02
 pay_xfer_fraction: 0.98
+
+# asset config must be 1.0
+asset_create_fraction: 1.0
diff --git a/idb/idb.go b/idb/idb.go
index bef8e74..19b854f 100644
--- a/idb/idb.go
+++ b/idb/idb.go
@@ -16,6 +16,7 @@ import (
 
 	models "github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/types"
+	"github.com/algorand/indexer/util"
 )
 
 // TxnRow is metadata relating to one transaction in a transaction query.
@@ -422,7 +423,7 @@ func (ad AppDelta) String() string {
 	}
 	ds := ""
 	if ad.Delta != nil {
-		ds = string(JSONOneLine(ad.Delta))
+		ds = string(util.JSONOneLine(ad.Delta))
 	}
 	parts = append(parts, fmt.Sprintf("ai=%d oc=%v d=%s", ad.AddrIndex, ad.OnCompletion, ds))
 	if len(ad.ApprovalProgram) > 0 {
diff --git a/idb/postgres/internal/encoding/decode.go b/idb/postgres/internal/encoding/decode.go
new file mode 100644
index 0000000..b5830b5
--- /dev/null
+++ b/idb/postgres/internal/encoding/decode.go
@@ -0,0 +1,8 @@
+package encoding
+
+import (
+	"github.com/algorand/go-algorand-sdk/encoding/json"
+)
+
+// DecodeJSON decodes a json string.
+var DecodeJSON = json.Decode
diff --git a/idb/postgres/internal/encoding/encode.go b/idb/postgres/internal/encoding/encode.go
new file mode 100644
index 0000000..e0c4fd7
--- /dev/null
+++ b/idb/postgres/internal/encoding/encode.go
@@ -0,0 +1,75 @@
+package encoding
+
+import (
+	"encoding/base64"
+
+	"github.com/algorand/go-codec/codec"
+
+	"github.com/algorand/indexer/types"
+)
+
+var jsonCodecHandle *codec.JsonHandle
+
+// EncodeJSON converts an object into JSON
+func EncodeJSON(obj interface{}) []byte {
+	var buf []byte
+	enc := codec.NewEncoderBytes(&buf, jsonCodecHandle)
+	enc.MustEncode(obj)
+	return buf
+}
+
+// Base64 encodes a byte array to a base64 string.
+func Base64(data []byte) string {
+	return base64.StdEncoding.EncodeToString(data)
+}
+
+func convertStateDelta(delta types.StateDelta) types.StateDelta {
+	if delta == nil {
+		return nil
+	}
+
+	res := make(map[string]types.ValueDelta, len(delta))
+	for k, v := range delta {
+		res[Base64([]byte(k))] = v
+	}
+	return res
+}
+
+func convertLocalDeltas(deltas map[uint64]types.StateDelta) map[uint64]types.StateDelta {
+	if deltas == nil {
+		return nil
+	}
+
+	res := make(map[uint64]types.StateDelta, len(deltas))
+	for i, delta := range deltas {
+		res[i] = convertStateDelta(delta)
+	}
+	return res
+}
+
+func convertEvalDelta(evalDelta types.EvalDelta) types.EvalDelta {
+	evalDelta.GlobalDelta = convertStateDelta(evalDelta.GlobalDelta)
+	evalDelta.LocalDeltas = convertLocalDeltas(evalDelta.LocalDeltas)
+	return evalDelta
+}
+
+func convertSignedTxnWithAD(stxn types.SignedTxnWithAD) types.SignedTxnWithAD {
+	stxn.EvalDelta = convertEvalDelta(stxn.EvalDelta)
+	return stxn
+}
+
+// EncodeSignedTxnWithAD returns a json string where all byte arrays are base64 encoded.
+func EncodeSignedTxnWithAD(stxn types.SignedTxnWithAD) []byte {
+	return EncodeJSON(convertSignedTxnWithAD(stxn))
+}
+
+func init() {
+	jsonCodecHandle = new(codec.JsonHandle)
+	jsonCodecHandle.ErrorIfNoField = true
+	jsonCodecHandle.ErrorIfNoArrayExpand = true
+	jsonCodecHandle.Canonical = true
+	jsonCodecHandle.RecursiveEmptyCheck = true
+	jsonCodecHandle.HTMLCharsAsIs = true
+	jsonCodecHandle.Indent = 0
+	jsonCodecHandle.MapKeyAsString = true
+}
diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go
index 428e49d..1315518 100644
--- a/idb/postgres/postgres.go
+++ b/idb/postgres/postgres.go
@@ -10,7 +10,6 @@ import (
 	"bytes"
 	"context"
 	"database/sql"
-	"encoding/base64"
 	"errors"
 	"fmt"
 	"math"
@@ -22,7 +21,6 @@ import (
 	"unicode/utf8"
 
 	"github.com/algorand/go-algorand-sdk/crypto"
-	"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"
 
@@ -33,6 +31,7 @@ import (
 	models "github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/idb/migration"
+	"github.com/algorand/indexer/idb/postgres/internal/encoding"
 	"github.com/algorand/indexer/importer"
 	"github.com/algorand/indexer/types"
 )
@@ -173,34 +172,10 @@ func (db *IndexerDb) StartBlock() (err error) {
 	return nil
 }
 
-// For App apply data, convert "string" keys which are secretly []byte blobs to their base64 representation so that JSON systems that require strings to be utf8 don't panic.
-func stxnToJSON(txn types.SignedTxnWithAD) string {
-	jt := txn
-	if len(jt.EvalDelta.GlobalDelta) > 0 {
-		gd := make(map[string]types.ValueDelta, len(jt.EvalDelta.GlobalDelta))
-		for k, v := range jt.EvalDelta.GlobalDelta {
-			gd[b64([]byte(k))] = v
-		}
-		jt.EvalDelta.GlobalDelta = gd
-	}
-	if len(jt.EvalDelta.LocalDeltas) > 0 {
-		ldout := make(map[uint64]types.StateDelta, len(jt.EvalDelta.LocalDeltas))
-		for i, ld := range jt.EvalDelta.LocalDeltas {
-			nld := make(map[string]types.ValueDelta, len(ld))
-			for k, v := range ld {
-				nld[b64([]byte(k))] = v
-			}
-			ldout[i] = nld
-		}
-		jt.EvalDelta.LocalDeltas = ldout
-	}
-	return idb.JSONOneLine(jt)
-}
-
 // AddTransaction is part of idb.IndexerDB
 func (db *IndexerDb) AddTransaction(round uint64, intra int, txtypeenum int, assetid uint64, txn types.SignedTxnWithAD, participation [][]byte) error {
 	txnbytes := msgpack.Encode(txn)
-	jsonbytes := stxnToJSON(txn)
+	jsonbytes := encoding.EncodeSignedTxnWithAD(txn)
 	txid := crypto.TransactionIDString(txn.Txn)
 	tx := []interface{}{round, intra, txtypeenum, assetid, txid[:], txnbytes, string(jsonbytes)}
 	db.txrows = append(db.txrows, tx)
@@ -261,7 +236,7 @@ func (db *IndexerDb) commitBlock(tx *sql.Tx, round uint64, timestamp int64, rewa
 		if err != nil {
 			//return err
 			for _, er := range db.txprows[:i+1] {
-				db.log.Printf("%s %d %d", base64.StdEncoding.EncodeToString(er[0].([]byte)), er[1], er[2])
+				db.log.Printf("%s %d %d", encoding.Base64(er[0].([]byte)), er[1], er[2])
 			}
 			return fmt.Errorf("%v, around txp row %#v", err, txpr)
 		}
@@ -281,7 +256,7 @@ func (db *IndexerDb) commitBlock(tx *sql.Tx, round uint64, timestamp int64, rewa
 	if err != nil {
 		return fmt.Errorf("decode header %v", err)
 	}
-	headerjson := idb.JSONOneLine(blockHeader)
+	headerjson := encoding.EncodeJSON(blockHeader)
 	_, err = tx.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`, round, time.Unix(timestamp, 0).UTC(), rewardslevel, headerjson)
 	if err != nil {
 		return fmt.Errorf("put block_header %v    %#v", err, err)
@@ -348,14 +323,14 @@ func (db *IndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
 		if len(alloc.State.AssetParams) > 0 || len(alloc.State.Assets) > 0 {
 			return fmt.Errorf("genesis account[%d] has unhandled asset", ai)
 		}
-		_, err = setAccount.Exec(addr[:], alloc.State.MicroAlgos, idb.JSONOneLine(alloc.State), 0)
+		_, err = setAccount.Exec(addr[:], alloc.State.MicroAlgos, encoding.EncodeJSON(alloc.State), 0)
 		total += uint64(alloc.State.MicroAlgos)
 		if err != nil {
 			return fmt.Errorf("error setting genesis account[%d], %v", ai, err)
 		}
 	}
 	var istate importer.ImportState
-	sjs := string(idb.JSONOneLine(istate))
+	sjs := string(encoding.EncodeJSON(istate))
 	_, err = tx.Exec(setMetastateUpsert, stateMetastateKey, sjs)
 	if err != nil {
 		return
@@ -398,7 +373,7 @@ func (db *IndexerDb) GetImportState() (state idb.ImportState, err error) {
 		return
 	}
 
-	err = json.Decode([]byte(importStateJSON), &state)
+	err = encoding.DecodeJSON([]byte(importStateJSON), &state)
 	if err != nil {
 		err = fmt.Errorf("unable to parse import state: %v", err)
 	}
@@ -407,7 +382,7 @@ func (db *IndexerDb) GetImportState() (state idb.ImportState, err error) {
 
 // SetImportState is part of idb.IndexerDB
 func (db *IndexerDb) SetImportState(state idb.ImportState) (err error) {
-	return db.setMetastate(stateMetastateKey, string(json.Encode(state)))
+	return db.setMetastate(stateMetastateKey, string(encoding.EncodeJSON(state)))
 }
 
 // If `tx` is null, make a standalone query.
@@ -547,7 +522,7 @@ func (db *IndexerDb) yieldTxnsThread(ctx context.Context, rows *sql.Rows, result
 			row.TxnBytes = txnbytess[i]
 			row.AssetID = uint64(creatableids[i])
 			if len(extrajsons[i]) > 0 {
-				err := json.Decode(extrajsons[i], &row.Extra)
+				err := encoding.DecodeJSON(extrajsons[i], &row.Extra)
 				if err != nil {
 					row.Error = fmt.Errorf("%d:%d decode txn extra, %v", row.Round, row.Intra, err)
 					results <- row
@@ -592,12 +567,8 @@ func (db *IndexerDb) YieldTxns(ctx context.Context, firstRound uint64) <-chan id
 // debug any asset that isn't working out right:
 var debugAsset uint64 = 0
 
-func b64(addr []byte) string {
-	return base64.StdEncoding.EncodeToString(addr)
-}
-
 func obs(x interface{}) string {
-	return idb.JSONOneLine(x)
+	return string(encoding.EncodeJSON(x))
 }
 
 // StateSchema like go-algorand data/basics/teal.go
@@ -654,7 +625,7 @@ func (tv TealValue) toModel() models.TealValue {
 	case TealUintType:
 		return models.TealValue{Uint: tv.Uint, Type: uint64(tv.Type)}
 	case TealBytesType:
-		return models.TealValue{Bytes: b64(tv.Bytes), Type: uint64(tv.Type)}
+		return models.TealValue{Bytes: encoding.Base64(tv.Bytes), Type: uint64(tv.Type)}
 	}
 	return models.TealValue{}
 }
@@ -679,7 +650,7 @@ func (tkv TealKeyValue) toModel() *models.TealKeyValueStore {
 	var out models.TealKeyValueStore = make([]models.TealKeyValue, len(tkv.They))
 	pos := 0
 	for _, ktv := range tkv.They {
-		out[pos].Key = b64(ktv.Key)
+		out[pos].Key = encoding.Base64(ktv.Key)
 		out[pos].Value = ktv.Tv.toModel()
 		pos++
 	}
@@ -719,14 +690,14 @@ func (tkv *TealKeyValue) delete(key []byte) {
 	}
 }
 
-// MarshalJSON wraps idb.JSONOneLine
+// MarshalJSON wraps encoding.EncodeJSON
 func (tkv TealKeyValue) MarshalJSON() ([]byte, error) {
-	return []byte(idb.JSONOneLine(tkv.They)), nil
+	return encoding.EncodeJSON(tkv.They), nil
 }
 
-// UnmarshalJSON wraps json.Decode
+// UnmarshalJSON wraps encoding.DecodeJSON
 func (tkv *TealKeyValue) UnmarshalJSON(data []byte) error {
-	return json.Decode(data, &tkv.They)
+	return encoding.DecodeJSON(data, &tkv.They)
 }
 
 // AppParams like go-algorand data/basics/userBalance.go AppParams{}
@@ -800,7 +771,7 @@ func (db *IndexerDb) getDirtyAppLocalState(addr []byte, appIndex int64, dirty []
 		err = fmt.Errorf("app local get, %v", err)
 		return
 	} else if len(localstatejson) > 0 {
-		err = json.Decode(localstatejson, &localstate.AppLocalState)
+		err = encoding.DecodeJSON(localstatejson, &localstate.AppLocalState)
 		if err != nil {
 			err = fmt.Errorf("app local get bad json, %v", err)
 		}
@@ -902,7 +873,7 @@ func (db *IndexerDb) commitRoundAccounting(tx *sql.Tx, updates idb.RoundUpdates,
 				}
 			}
 
-			jb := idb.JSONOneLine(set)
+			jb := encoding.EncodeJSON(set)
 			_, err = setad.Exec(jb, addr[:])
 			if err != nil {
 				return fmt.Errorf("update account data, %v", err)
@@ -976,7 +947,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 			for addr, aulist := range subround {
 				for _, au := range aulist {
 					if au.AssetID == debugAsset {
-						db.log.Errorf("%d axfer %s %s", round, b64(addr[:]), obs(au))
+						db.log.Errorf("%d axfer %s %s", round, encoding.Base64(addr[:]), obs(au))
 					}
 
 					// Apply deltas
@@ -1037,9 +1008,9 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 
 					// Asset Config
 					if au.Config != nil {
-						var outparams string
+						var outparams []byte
 						if au.Config.IsNew {
-							outparams = string(json.Encode(au.Config.Params))
+							outparams = encoding.EncodeJSON(au.Config.Params)
 						} else {
 							row := getacfg.QueryRow(au.AssetID)
 							var paramjson []byte
@@ -1048,12 +1019,12 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 								return fmt.Errorf("get acfg %d, %v", au.AssetID, err)
 							}
 							var old sdk_types.AssetParams
-							err = json.Decode(paramjson, &old)
+							err = encoding.DecodeJSON(paramjson, &old)
 							if err != nil {
 								return fmt.Errorf("bad acgf json %d, %v", au.AssetID, err)
 							}
 							np := types.MergeAssetConfig(old, au.Config.Params)
-							outparams = string(json.Encode(np))
+							outparams = encoding.EncodeJSON(np)
 						}
 						_, err = setacfg.Exec(au.AssetID, au.Config.Creator[:], outparams, round)
 						if err != nil {
@@ -1064,7 +1035,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 					// Asset Freeze
 					if au.Freeze != nil {
 						if au.AssetID == debugAsset {
-							db.log.Errorf("%d %s %s", round, b64(addr[:]), obs(au.Freeze))
+							db.log.Errorf("%d %s %s", round, encoding.Base64(addr[:]), obs(au.Freeze))
 						}
 						_, err = fr.Exec(addr[:], au.AssetID, au.Freeze.Frozen, round)
 						if err != nil {
@@ -1127,7 +1098,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 				} else if err != nil {
 					return fmt.Errorf("app[%d] global get, %v", adelta.AppIndex, err)
 				} else {
-					err = json.Decode(paramsjson, &state)
+					err = encoding.DecodeJSON(paramsjson, &state)
 					if err != nil {
 						return fmt.Errorf("app[%d] global get json, %v", adelta.AppIndex, err)
 					}
@@ -1153,7 +1124,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 					return fmt.Errorf("app delta apply err r=%d i=%d app=%d, %v", adelta.Round, adelta.Intra, adelta.AppIndex, err)
 				}
 			}
-			reverseDeltas = append(reverseDeltas, []interface{}{idb.JSONOneLine(reverseDelta), adelta.Round, adelta.Intra})
+			reverseDeltas = append(reverseDeltas, []interface{}{encoding.EncodeJSON(reverseDelta), adelta.Round, adelta.Intra})
 			if adelta.OnCompletion == sdk_types.DeleteApplicationOC {
 				// clear content but leave row recording that it existed
 				state = AppParams{}
@@ -1193,7 +1164,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 				Valid: destroy[appid],
 			}
 			creator := appCreators[appid]
-			paramjson := idb.JSONOneLine(params)
+			paramjson := encoding.EncodeJSON(params)
 			_, err = putglobal.Exec(appid, creator, paramjson, round, closedAt, destroy[appid])
 			if err != nil {
 				return fmt.Errorf("app global put pj=%v, %v", string(paramjson), err)
@@ -1235,7 +1206,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 					return fmt.Errorf("app get (l), %v", err)
 				}
 				var app AppParams
-				err = json.Decode(paramsjson, &app)
+				err = encoding.DecodeJSON(paramsjson, &app)
 				if err != nil {
 					return fmt.Errorf("app[%d] get json (l), %v", ald.AppIndex, err)
 				}
@@ -1251,7 +1222,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 				}
 			}
 			dirty = setDirtyAppLocalState(dirty, localstate)
-			reverseDeltas = append(reverseDeltas, []interface{}{idb.JSONOneLine(reverseDelta), ald.Round, ald.Intra})
+			reverseDeltas = append(reverseDeltas, []interface{}{encoding.EncodeJSON(reverseDelta), ald.Round, ald.Intra})
 		}
 
 		// update txns with reverse deltas
@@ -1278,7 +1249,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 			}
 			defer putglobal.Close()
 			for _, ld := range dirty {
-				_, err = putglobal.Exec(ld.address, ld.appIndex, idb.JSONOneLine(ld.AppLocalState), round)
+				_, err = putglobal.Exec(ld.address, ld.appIndex, encoding.EncodeJSON(ld.AppLocalState), round)
 				if err != nil {
 					return fmt.Errorf("app local put, %v", err)
 				}
@@ -1311,7 +1282,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 	} else if err != nil {
 		return
 	} else {
-		err = json.Decode([]byte(stateJSONStr), &istate)
+		err = encoding.DecodeJSON([]byte(stateJSONStr), &istate)
 		if err != nil {
 			return
 		}
@@ -1322,7 +1293,7 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 		return errors.New(msg)
 	}
 	istate.AccountRound = int64(round)
-	sjs := idb.JSONOneLine(istate)
+	sjs := encoding.EncodeJSON(istate)
 	_, err = tx.Exec(setMetastateUpsert, stateMetastateKey, sjs)
 	if err != nil {
 		return
@@ -1354,7 +1325,7 @@ func (db *IndexerDb) GetBlock(ctx context.Context, round uint64, options idb.Get
 	if err != nil {
 		return
 	}
-	err = json.Decode(blockheaderjson, &blockHeader)
+	err = encoding.DecodeJSON(blockheaderjson, &blockHeader)
 	if err != nil {
 		return
 	}
@@ -1404,7 +1375,7 @@ func buildTransactionQuery(tf idb.TransactionFilter) (query string, whereArgs []
 		whereArgs = append(whereArgs, tf.Address)
 		partNumber++
 		if tf.AddressRole != 0 {
-			addrBase64 := base64.StdEncoding.EncodeToString(tf.Address)
+			addrBase64 := encoding.Base64(tf.Address)
 			roleparts := make([]string, 0, 8)
 			if tf.AddressRole&idb.AddressRoleSender != 0 {
 				roleparts = append(roleparts, fmt.Sprintf("t.txn -> 'txn' ->> 'snd' = $%d", partNumber))
@@ -1755,7 +1726,7 @@ func (db *IndexerDb) yieldTxnsThreadSimple(ctx context.Context, rows *sql.Rows,
 			row.RoundTime = roundtime
 			row.AssetID = asset
 			if len(extraJSON) > 0 {
-				err = json.Decode(extraJSON, &row.Extra)
+				err = encoding.DecodeJSON(extraJSON, &row.Extra)
 				if err != nil {
 					row.Error = fmt.Errorf("%d:%d decode txn extra, %v", row.Round, row.Intra, err)
 				}
@@ -1898,7 +1869,7 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 
 		if accountDataJSONStr != nil {
 			var ad types.AccountData
-			err = json.Decode(accountDataJSONStr, &ad)
+			err = encoding.DecodeJSON(accountDataJSONStr, &ad)
 			if err != nil {
 				err = fmt.Errorf("account decode err (%s) %v", accountDataJSONStr, err)
 				req.out <- idb.AccountRow{Error: err}
@@ -1952,42 +1923,42 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 
 		if len(holdingAssetids) > 0 && string(holdingAssetids) != nullarraystr {
 			var haids []uint64
-			err = json.Decode(holdingAssetids, &haids)
+			err = encoding.DecodeJSON(holdingAssetids, &haids)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding asset ids err %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var hamounts []uint64
-			err = json.Decode(holdingAmount, &hamounts)
+			err = encoding.DecodeJSON(holdingAmount, &hamounts)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding amounts err %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var hfrozen []bool
-			err = json.Decode(holdingFrozen, &hfrozen)
+			err = encoding.DecodeJSON(holdingFrozen, &hfrozen)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding frozen err %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var holdingCreated []*uint64
-			err = json.Decode(holdingCreatedBytes, &holdingCreated)
+			err = encoding.DecodeJSON(holdingCreatedBytes, &holdingCreated)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding created ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var holdingClosed []*uint64
-			err = json.Decode(holdingClosedBytes, &holdingClosed)
+			err = encoding.DecodeJSON(holdingClosedBytes, &holdingClosed)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding closed ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var holdingDeleted []*bool
-			err = json.Decode(holdingDeletedBytes, &holdingDeleted)
+			err = encoding.DecodeJSON(holdingDeletedBytes, &holdingDeleted)
 			if err != nil {
 				err = fmt.Errorf("parsing json holding deleted ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2029,35 +2000,35 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 		}
 		if len(assetParamsIds) > 0 && string(assetParamsIds) != nullarraystr {
 			var assetids []uint64
-			err = json.Decode(assetParamsIds, &assetids)
+			err = encoding.DecodeJSON(assetParamsIds, &assetids)
 			if err != nil {
 				err = fmt.Errorf("parsing json asset param ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var assetParams []types.AssetParams
-			err = json.Decode(assetParamsStr, &assetParams)
+			err = encoding.DecodeJSON(assetParamsStr, &assetParams)
 			if err != nil {
 				err = fmt.Errorf("parsing json asset param string, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var assetCreated []*uint64
-			err = json.Decode(assetParamsCreatedBytes, &assetCreated)
+			err = encoding.DecodeJSON(assetParamsCreatedBytes, &assetCreated)
 			if err != nil {
 				err = fmt.Errorf("parsing json asset created ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var assetClosed []*uint64
-			err = json.Decode(assetParamsClosedBytes, &assetClosed)
+			err = encoding.DecodeJSON(assetParamsClosedBytes, &assetClosed)
 			if err != nil {
 				err = fmt.Errorf("parsing json asset closed ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var assetDeleted []*bool
-			err = json.Decode(assetParamsDeletedBytes, &assetDeleted)
+			err = encoding.DecodeJSON(assetParamsDeletedBytes, &assetDeleted)
 			if err != nil {
 				err = fmt.Errorf("parsing json asset deleted ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2115,28 +2086,28 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 		if len(appParamIndexes) > 0 {
 			// apps owned by this account
 			var appIds []uint64
-			err = json.Decode(appParamIndexes, &appIds)
+			err = encoding.DecodeJSON(appParamIndexes, &appIds)
 			if err != nil {
 				err = fmt.Errorf("parsing json appids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appCreated []*uint64
-			err = json.Decode(appCreatedBytes, &appCreated)
+			err = encoding.DecodeJSON(appCreatedBytes, &appCreated)
 			if err != nil {
 				err = fmt.Errorf("parsing json app created ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appClosed []*uint64
-			err = json.Decode(appClosedBytes, &appClosed)
+			err = encoding.DecodeJSON(appClosedBytes, &appClosed)
 			if err != nil {
 				err = fmt.Errorf("parsing json app closed ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appDeleted []*bool
-			err = json.Decode(appDeletedBytes, &appDeleted)
+			err = encoding.DecodeJSON(appDeletedBytes, &appDeleted)
 			if err != nil {
 				err = fmt.Errorf("parsing json app deleted flags, %v", err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2144,7 +2115,7 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 			}
 
 			var apps []AppParams
-			err = json.Decode(appParams, &apps)
+			err = encoding.DecodeJSON(appParams, &apps)
 			if err != nil {
 				err = fmt.Errorf("parsing json appparams, %v", err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2191,35 +2162,35 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) {
 
 		if len(localStateAppIds) > 0 {
 			var appIds []uint64
-			err = json.Decode(localStateAppIds, &appIds)
+			err = encoding.DecodeJSON(localStateAppIds, &appIds)
 			if err != nil {
 				err = fmt.Errorf("parsing json local appids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appCreated []*uint64
-			err = json.Decode(localStateCreatedBytes, &appCreated)
+			err = encoding.DecodeJSON(localStateCreatedBytes, &appCreated)
 			if err != nil {
 				err = fmt.Errorf("parsing json ls created ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appClosed []*uint64
-			err = json.Decode(localStateClosedBytes, &appClosed)
+			err = encoding.DecodeJSON(localStateClosedBytes, &appClosed)
 			if err != nil {
 				err = fmt.Errorf("parsing json ls closed ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var appDeleted []*bool
-			err = json.Decode(localStateDeletedBytes, &appDeleted)
+			err = encoding.DecodeJSON(localStateDeletedBytes, &appDeleted)
 			if err != nil {
 				err = fmt.Errorf("parsing json ls closed ids, %v", err)
 				req.out <- idb.AccountRow{Error: err}
 				break
 			}
 			var ls []AppLocalState
-			err = json.Decode(localStates, &ls)
+			err = encoding.DecodeJSON(localStates, &ls)
 			if err != nil {
 				err = fmt.Errorf("parsing json local states, %v", err)
 				req.out <- idb.AccountRow{Error: err}
@@ -2398,7 +2369,7 @@ func (db *IndexerDb) GetAccounts(ctx context.Context, opts idb.AccountQueryOptio
 		return out, round
 	}
 	var blockheader types.BlockHeader
-	err = json.Decode(headerjson, &blockheader)
+	err = encoding.DecodeJSON(headerjson, &blockheader)
 	if err != nil {
 		err = fmt.Errorf("account round header %d err %v", round, err)
 		out <- idb.AccountRow{Error: err}
@@ -2656,7 +2627,7 @@ func (db *IndexerDb) yieldAssetsThread(ctx context.Context, filter idb.AssetsQue
 			break
 		}
 		var params types.AssetParams
-		err = json.Decode(paramsJSONStr, &params)
+		err = encoding.DecodeJSON(paramsJSONStr, &params)
 		if err != nil {
 			out <- idb.AssetRow{Error: err}
 			break
@@ -2873,7 +2844,7 @@ func (db *IndexerDb) yieldApplicationsThread(ctx context.Context, rows *sql.Rows
 		rec.Application.DeletedAtRound = closed
 		rec.Application.Deleted = deleted
 		var ap AppParams
-		err = json.Decode(paramsjson, &ap)
+		err = encoding.DecodeJSON(paramsjson, &ap)
 		if err != nil {
 			rec.Error = fmt.Errorf("app=%d json err, %v", index, err)
 			out <- rec
@@ -2970,8 +2941,8 @@ func (db *IndexerDb) GetSpecialAccounts() (accounts idb.SpecialAccounts, err err
 			RewardsPool: blockHeader.RewardsPool,
 		}
 
-		cache := idb.JSONOneLine(accounts)
-		err = db.setMetastate(specialAccountsMetastateKey, cache)
+		cache := encoding.EncodeJSON(accounts)
+		err = db.setMetastate(specialAccountsMetastateKey, string(cache))
 		if err != nil {
 			return idb.SpecialAccounts{}, fmt.Errorf("problem saving metastate: %v", err)
 		}
@@ -2979,7 +2950,7 @@ func (db *IndexerDb) GetSpecialAccounts() (accounts idb.SpecialAccounts, err err
 		return
 	}
 
-	err = json.Decode([]byte(cache), &accounts)
+	err = encoding.DecodeJSON([]byte(cache), &accounts)
 	if err != nil {
 		err = fmt.Errorf("problem decoding cache '%s': %v", cache, err)
 	}
diff --git a/idb/postgres/postgres_migrations.go b/idb/postgres/postgres_migrations.go
index 5d811a5..0b4519a 100644
--- a/idb/postgres/postgres_migrations.go
+++ b/idb/postgres/postgres_migrations.go
@@ -7,14 +7,12 @@ import (
 	"bytes"
 	"context"
 	"database/sql"
-	"encoding/base64"
 	"fmt"
 	"math"
 	"os"
 	"time"
 
 	"github.com/algorand/go-algorand-sdk/crypto"
-	"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"
 
@@ -22,6 +20,7 @@ import (
 	"github.com/algorand/indexer/api/generated/v2"
 	"github.com/algorand/indexer/idb"
 	"github.com/algorand/indexer/idb/migration"
+	"github.com/algorand/indexer/idb/postgres/internal/encoding"
 	"github.com/algorand/indexer/types"
 )
 
@@ -120,7 +119,7 @@ func upsertMigrationStateTx(tx *sql.Tx, state *MigrationState, incrementNextMigr
 	if incrementNextMigration {
 		state.NextMigration++
 	}
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 
 	return err
@@ -131,7 +130,7 @@ func upsertMigrationState(db *IndexerDb, state *MigrationState, incrementNextMig
 	if incrementNextMigration {
 		state.NextMigration++
 	}
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 
 	return err
@@ -140,7 +139,7 @@ func upsertMigrationState(db *IndexerDb, state *MigrationState, incrementNextMig
 func (db *IndexerDb) runAvailableMigrations(migrationStateJSON string) (err error) {
 	var state MigrationState
 	if len(migrationStateJSON) > 0 {
-		err = json.Decode([]byte(migrationStateJSON), &state)
+		err = encoding.DecodeJSON([]byte(migrationStateJSON), &state)
 		if err != nil {
 			return fmt.Errorf("(%s) bad metastate migration json, %v", migrationStateJSON, err)
 		}
@@ -185,8 +184,8 @@ func (db *IndexerDb) markMigrationsAsDone() (err error) {
 	state := MigrationState{
 		NextMigration: len(migrations),
 	}
-	migrationStateJSON := idb.JSONOneLine(state)
-	return db.setMetastate(migrationMetastateKey, migrationStateJSON)
+	migrationStateJSON := encoding.EncodeJSON(state)
+	return db.setMetastate(migrationMetastateKey, string(migrationStateJSON))
 }
 
 func (db *IndexerDb) getMigrationState() (*MigrationState, error) {
@@ -198,7 +197,7 @@ func (db *IndexerDb) getMigrationState() (*MigrationState, error) {
 		return nil, fmt.Errorf("%s, get m state err", txidMigrationErrMsg)
 	}
 	var txstate MigrationState
-	err = json.Decode([]byte(migrationStateJSON), &txstate)
+	err = encoding.DecodeJSON([]byte(migrationStateJSON), &txstate)
 	if err != nil {
 		return nil, fmt.Errorf("%s, migration json err", txidMigrationErrMsg)
 	}
@@ -332,7 +331,7 @@ func m3acfgFixAsyncInner(db *IndexerDb, state *MigrationState, assetIds []int64)
 		if now.Sub(lastlog) > (5 * time.Second) {
 			db.log.Printf("acfg fix next=%d", aid)
 			state.NextAssetID = aid
-			migrationStateJSON := idb.JSONOneLine(state)
+			migrationStateJSON := encoding.EncodeJSON(state)
 			_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 			if err != nil {
 				db.log.WithError(err).Errorf("acfg fix migration %d meta err", state.NextMigration)
@@ -372,7 +371,7 @@ func m3acfgFixAsyncInner(db *IndexerDb, state *MigrationState, assetIds []int64)
 				params = types.MergeAssetConfig(params, stxn.Txn.AssetParams)
 			}
 		}
-		outparams := idb.JSONOneLine(params)
+		outparams := encoding.EncodeJSON(params)
 		_, err = setacfg.Exec(aid, creator[:], outparams)
 		if err != nil {
 			db.log.WithError(err).Errorf("acfg fix asset update")
@@ -381,7 +380,7 @@ func m3acfgFixAsyncInner(db *IndexerDb, state *MigrationState, assetIds []int64)
 	}
 	state.NextAssetID = 0
 	state.NextMigration++
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		db.log.WithError(err).Errorf("acfg fix migration %d meta err", state.NextMigration)
@@ -824,7 +823,7 @@ func m7RewardsAndDatesPart2UpdateAccounts(db *IndexerDb, accountData []addressAc
 		state.PointerRound = &round
 		state.PointerIntra = &intra
 	}
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		return fmt.Errorf("m7: failed to update migration checkpoint: %v", err)
@@ -1151,7 +1150,7 @@ func m7RewardsAndDatesPart2(db *IndexerDb, state *MigrationState) error {
 	state.NextRound = 0
 	state.PointerRound = nil
 	state.PointerIntra = nil
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err := db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		return fmt.Errorf("m7: failed to write final migration state: %v", err)
@@ -1180,7 +1179,7 @@ func sqlMigration(db *IndexerDb, state *MigrationState, sqlLines []string) error
 		}
 	}
 	state.NextMigration++
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		db.log.WithError(err).Errorf("migration %d meta err", thisMigration)
@@ -1268,8 +1267,8 @@ func (mtxid *txidFiuxpMigrationContext) asyncTxidFixup() (err error) {
 	// all done, mark migration state
 	state.NextMigration++
 	state.NextRound = 0
-	migrationStateJSON := idb.JSONOneLine(state)
-	err = db.setMetastate(migrationMetastateKey, migrationStateJSON)
+	migrationStateJSON := encoding.EncodeJSON(state)
+	err = db.setMetastate(migrationMetastateKey, string(migrationStateJSON))
 	if err != nil {
 		db.log.WithError(err).Errorf("%s, error setting final migration state", txidMigrationErrMsg)
 		return
@@ -1389,7 +1388,7 @@ func (mtxid *txidFiuxpMigrationContext) putTxidFixupBatch(batch []idb.TxnRow) er
 		return err
 	}
 	txstate.NextRound = int64(maxRound + 1)
-	migrationStateJSON := idb.JSONOneLine(txstate)
+	migrationStateJSON := encoding.EncodeJSON(txstate)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		db.log.WithError(err).Errorf("%s, set metastate err", txidMigrationErrMsg)
@@ -1437,7 +1436,7 @@ func readHeaders(db *IndexerDb, minRound, maxRound uint64) (map[uint64]types.Blo
 			return nil, err
 		}
 		var tblock types.Block
-		json.Decode(headerjson, &tblock)
+		encoding.DecodeJSON(headerjson, &tblock)
 		headers[uint64(round)] = tblock
 	}
 	if err := rows.Err(); err != nil {
@@ -1589,7 +1588,7 @@ func m9TxnJSONEncoding(db *IndexerDb, state *MigrationState) (err error) {
 	if err == sql.ErrNoRows {
 		// Indexer may be new after m7, marking it as done without running it, so we don't need to do anything here.
 		state.NextMigration++
-		migrationStateJSON := json.Encode(state)
+		migrationStateJSON := encoding.EncodeJSON(state)
 		_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 		if err != nil {
 			db.log.WithError(err).Errorf("%s, meta err", m9ErrPrefix)
@@ -1665,7 +1664,7 @@ func m9TxnJSONEncoding(db *IndexerDb, state *MigrationState) (err error) {
 	// all done, mark migration state
 	state.NextMigration++
 	state.NextRound = 0
-	migrationStateJSON := string(json.Encode(state))
+	migrationStateJSON := string(encoding.EncodeJSON(state))
 	err = db.setMetastate(migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		db.log.WithError(err).Errorf("%s, error setting final migration state", m9ErrPrefix)
@@ -1717,7 +1716,7 @@ func putTxnJSONBatch(db *IndexerDb, state *MigrationState, batch []jsonFixupTxnR
 		if stxn.HasGenesisHash || proto.RequireGenesisHash {
 			stxn.Txn.GenesisHash = block.GenesisHash
 		}
-		js := stxnToJSON(stxn.SignedTxnWithAD)
+		js := string(encoding.EncodeSignedTxnWithAD(stxn.SignedTxnWithAD))
 		if js == string(txr.JSON) {
 			outrows[pos].round = txr.Round
 			outrows[pos].intra = txr.Intra
@@ -1791,7 +1790,7 @@ func putTxnJSONBatch(db *IndexerDb, state *MigrationState, batch []jsonFixupTxnR
 		return err
 	}
 	txstate.NextRound = int64(maxRound + 1)
-	migrationStateJSON := json.Encode(txstate)
+	migrationStateJSON := encoding.EncodeJSON(txstate)
 	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		db.log.WithError(err).Errorf("%s, set metastate err", m9ErrPrefix)
@@ -1876,7 +1875,7 @@ func updateFrozenState(db *IndexerDb, assetID uint64, closedAt *uint64, creator,
 		minRound = *closedAt
 	}
 
-	holderb64 := base64.StdEncoding.EncodeToString(holder[:])
+	holderb64 := encoding.Base64(holder[:])
 	row := db.db.QueryRow(freezeTransactionsQuery, freeze[:], holderb64, assetID, minRound)
 	var found uint64
 	err := row.Scan(&found)
@@ -2197,7 +2196,7 @@ func ClearAccountDataMigration(db *IndexerDb, state *MigrationState) error {
 	}
 
 	state.NextMigration++
-	migrationStateJSON := idb.JSONOneLine(state)
+	migrationStateJSON := encoding.EncodeJSON(state)
 	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 	if err != nil {
 		return fmt.Errorf("metastate upsert error: %v", err)
diff --git a/idb/postgres/setup_postgres_sql.go b/idb/postgres/setup_postgres_sql.go
index defb905..91625e3 100644
--- a/idb/postgres/setup_postgres_sql.go
+++ b/idb/postgres/setup_postgres_sql.go
@@ -22,9 +22,9 @@ round bigint NOT NULL,
 intra smallint NOT NULL,
 typeenum smallint NOT NULL,
 asset bigint NOT NULL, -- 0=Algos, otherwise AssetIndex
-txid bytea NOT NULL, -- base32 of [32]byte hash + [4]byte checksum
-txnbytes bytea NOT NULL,
-txn jsonb NOT NULL,
+txid bytea NOT NULL, -- base32 of [32]byte hash
+txnbytes bytea NOT NULL, -- msgpack encoding of signed txn with apply data
+txn jsonb NOT NULL, -- json encoding of signed txn with apply data
 extra jsonb,
 PRIMARY KEY ( round, intra )
 );
diff --git a/idb/transcode.go b/idb/transcode.go
deleted file mode 100644
index 90a0e7e..0000000
--- a/idb/transcode.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package idb
-
-import (
-	"github.com/algorand/go-codec/codec"
-)
-
-// JSONOneLine converts an object into JSON
-func JSONOneLine(obj interface{}) string {
-	var b []byte
-	enc := codec.NewEncoderBytes(&b, oneLineJSONCodecHandle)
-	enc.MustEncode(obj)
-	return string(b)
-}
-
-var oneLineJSONCodecHandle *codec.JsonHandle
-
-func init() {
-	oneLineJSONCodecHandle = new(codec.JsonHandle)
-	oneLineJSONCodecHandle.ErrorIfNoField = true
-	oneLineJSONCodecHandle.ErrorIfNoArrayExpand = true
-	oneLineJSONCodecHandle.Canonical = true
-	oneLineJSONCodecHandle.RecursiveEmptyCheck = true
-	oneLineJSONCodecHandle.HTMLCharsAsIs = true
-	oneLineJSONCodecHandle.Indent = 0
-	oneLineJSONCodecHandle.MapKeyAsString = true
-}
diff --git a/util/util.go b/util/util.go
index 7420adf..984c728 100644
--- a/util/util.go
+++ b/util/util.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"os"
 	"strings"
+
+	"github.com/algorand/go-codec/codec"
 )
 
 // KeysStringInt returns all of the keys in the map joined by a comma.
@@ -33,3 +35,24 @@ func MaybeFail(err error, errfmt string, params ...interface{}) {
 	fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
 	os.Exit(1)
 }
+
+var oneLineJSONCodecHandle *codec.JsonHandle
+
+// JSONOneLine converts an object into JSON
+func JSONOneLine(obj interface{}) string {
+	var b []byte
+	enc := codec.NewEncoderBytes(&b, oneLineJSONCodecHandle)
+	enc.MustEncode(obj)
+	return string(b)
+}
+
+func init() {
+	oneLineJSONCodecHandle = new(codec.JsonHandle)
+	oneLineJSONCodecHandle.ErrorIfNoField = true
+	oneLineJSONCodecHandle.ErrorIfNoArrayExpand = true
+	oneLineJSONCodecHandle.Canonical = true
+	oneLineJSONCodecHandle.RecursiveEmptyCheck = true
+	oneLineJSONCodecHandle.HTMLCharsAsIs = true
+	oneLineJSONCodecHandle.Indent = 0
+	oneLineJSONCodecHandle.MapKeyAsString = true
+}


Test patch:
diff --git a/cmd/block-generator/generator/generate_test.go b/cmd/block-generator/generator/generate_test.go
new file mode 100644
index 0000000..d9830fb
--- /dev/null
+++ b/cmd/block-generator/generator/generate_test.go
@@ -0,0 +1,124 @@
+package generator
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/algorand/go-algorand-sdk/types"
+)
+
+func makePrivateGenerator(t *testing.T) *generator {
+	publicGenerator, err := MakeGenerator(GenerationConfig{
+		NumGenesisAccounts:           10,
+		GenesisAccountInitialBalance: 10000000000000000000,
+		PaymentTransactionFraction:   1.0,
+		PaymentNewAccountFraction:    1.0,
+		AssetCreateFraction:          1.0,
+	})
+	require.NoError(t, err)
+	return publicGenerator.(*generator)
+}
+
+func TestAssetXferNoAssetsOverride(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+
+	// First asset transaction must create.
+	txn := g.generateAssetTxnInternal(assetXfer, sp, 0)
+	require.Equal(t, types.AssetConfigTx, txn.Type)
+	require.Len(t, g.assets, 1)
+	require.Len(t, g.assets[0].holdings, 1)
+	require.Len(t, g.assets[0].holders, 1)
+}
+
+func TestAssetXferOneHolderOverride(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+
+	// Transfer converted to optin if there is only 1 holder.
+	txn := g.generateAssetTxnInternal(assetXfer, sp, 0)
+	require.Equal(t, types.AssetTransferTx, txn.Type)
+	require.Len(t, g.assets, 1)
+	// A new holding is created, indicating the optin
+	require.Len(t, g.assets[0].holdings, 2)
+	require.Len(t, g.assets[0].holders, 2)
+}
+
+func TestAssetCloseCreatorOverride(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+
+	// Instead of closing the creator, optin a new account
+	txn := g.generateAssetTxnInternal(assetClose, sp, 0)
+	require.Equal(t, types.AssetTransferTx, txn.Type)
+	require.Len(t, g.assets, 1)
+	// A new holding is created, indicating the optin
+	require.Len(t, g.assets[0].holdings, 2)
+	require.Len(t, g.assets[0].holders, 2)
+}
+
+func TestAssetOptinEveryAccountOverride(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+
+	// Opt all the accounts in, this also verifies that no account is opted in twice
+	var txn types.Transaction
+	for i := 2; uint64(i) <= g.numAccounts; i++ {
+		txn = g.generateAssetTxnInternal(assetOptin, sp, 0)
+		require.Equal(t, types.AssetTransferTx, txn.Type)
+		require.Len(t, g.assets, 1)
+		require.Len(t, g.assets[0].holdings, i)
+		require.Len(t, g.assets[0].holders, i)
+	}
+
+	// All accounts have opted in
+	require.Equal(t, g.numAccounts, uint64(len(g.assets[0].holdings)))
+
+	// The next optin closes instead
+	txn = g.generateAssetTxnInternal(assetOptin, sp, 0)
+	require.Equal(t, types.AssetTransferTx, txn.Type)
+	require.Len(t, g.assets, 1)
+	require.Len(t, g.assets[0].holdings, int(g.numAccounts-1))
+	require.Len(t, g.assets[0].holders, int(g.numAccounts-1))
+}
+
+func TestAssetDestroyWithHoldingsOverride(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+	g.generateAssetTxnInternal(assetOptin, sp, 0)
+	g.generateAssetTxnInternal(assetXfer, sp, 0)
+	require.Len(t, g.assets[0].holdings, 2)
+	require.Len(t, g.assets[0].holders, 2)
+
+	txn := g.generateAssetTxnInternal(assetDestroy, sp, 0)
+	require.Equal(t, types.AssetTransferTx, txn.Type)
+	require.Len(t, g.assets, 1)
+	require.Len(t, g.assets[0].holdings, 1)
+	require.Len(t, g.assets[0].holders, 1)
+}
+
+func TestAssetTransfer(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+	g.generateAssetTxnInternal(assetOptin, sp, 0)
+	g.generateAssetTxnInternal(assetXfer, sp, 0)
+	require.Greater(t, g.assets[0].holdings[1].balance, uint64(0))
+}
+
+func TestAssetDestroy(t *testing.T) {
+	g := makePrivateGenerator(t)
+	sp := g.getSuggestedParams(0)
+	g.generateAssetTxnInternal(assetCreate, sp, 0)
+	require.Len(t, g.assets, 1)
+
+	txn := g.generateAssetTxnInternal(assetDestroy, sp, 0)
+	require.Equal(t, types.AssetConfigTx, txn.Type)
+	require.Len(t, g.assets, 0)
+}
diff --git a/cmd/block-generator/generator/utils_test.go b/cmd/block-generator/generator/utils_test.go
new file mode 100644
index 0000000..278a1f2
--- /dev/null
+++ b/cmd/block-generator/generator/utils_test.go
@@ -0,0 +1,83 @@
+package generator
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestWeightedSelectionInternal(t *testing.T) {
+	weights := []float32{0.10, 0.30, 0.60}
+	options := []interface{}{"10", "30", "60"}
+
+	testcases := []struct {
+		selectionNum float32
+		expected     interface{}
+	}{
+		{
+			selectionNum: 0.0,
+			expected:     options[0],
+		},
+		{
+			selectionNum: 0.099,
+			expected:     options[0],
+		},
+		{
+			selectionNum: 0.1,
+			expected:     options[1],
+		},
+		{
+			selectionNum: 0.399,
+			expected:     options[1],
+		},
+		{
+			selectionNum: 0.4,
+			expected:     options[2],
+		},
+		{
+			selectionNum: 0.999,
+			expected:     options[2],
+		},
+	}
+
+	for _, test := range testcases {
+		name := fmt.Sprintf("selectionNum %f - expected %v", test.selectionNum, test.expected)
+		t.Run(name, func(t *testing.T) {
+			actual, err := weightedSelectionInternal(test.selectionNum, weights, options, nil)
+			require.NoError(t, err)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func TestWeightedSelection(t *testing.T) {
+	weights := []float32{0.10, 0.30, 0.60}
+	options := []interface{}{"10", "30", "60"}
+	selections := make(map[interface{}]int)
+
+	for i := 0; i < 100; i++ {
+		selected, err := weightedSelection(weights, options, nil)
+		require.NoError(t, err)
+		selections[selected]++
+	}
+
+	assert.Less(t, selections[options[0]], selections[options[1]])
+	assert.Less(t, selections[options[1]], selections[options[2]])
+}
+
+func TestWeightedSelectionOutOfRange(t *testing.T) {
+	weights := []float32{0.1}
+	options := []interface{}{"1"}
+	defaultOption := "DEFAULT!"
+
+	for i := 0; i < 10000; i++ {
+		selection, err := weightedSelection(weights, options, defaultOption)
+		require.NoError(t, err)
+		if selection == defaultOption {
+			return
+		}
+	}
+	assert.Fail(t, "Expected an out of range error by this point.")
+}
diff --git a/idb/postgres/internal/encoding/encoding_test.go b/idb/postgres/internal/encoding/encoding_test.go
new file mode 100644
index 0000000..60aa246
--- /dev/null
+++ b/idb/postgres/internal/encoding/encoding_test.go
@@ -0,0 +1,86 @@
+package encoding
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/algorand/indexer/types"
+)
+
+func TestEncodeSignedTxnWithAD(t *testing.T) {
+	type txnMsgpackJSON struct {
+		msgpack []byte
+		json    string
+	}
+
+	var testTxns = []txnMsgpackJSON{
+		{
+			[]uint8{0x83, 0xa2, 0x64, 0x74, 0x81, 0xa2, 0x67, 0x64, 0x81, 0xa9, 0xfe, 0xfe, 0xff, 0xef, 0x0, 0x0, 0x11, 0x22, 0x33, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0x3, 0x78, 0x78, 0x78, 0xa3, 0x73, 0x69, 0x67, 0xc4, 0x40, 0x51, 0xca, 0x9f, 0x32, 0xca, 0x9d, 0x66, 0x4b, 0xde, 0xa0, 0x98, 0xd9, 0x1b, 0xd, 0xe, 0x4d, 0x39, 0xca, 0x2, 0x4c, 0x4e, 0xc4, 0xba, 0x88, 0x1a, 0xb6, 0xa, 0x63, 0xff, 0xb0, 0x95, 0xc6, 0xb6, 0x7d, 0x0, 0xb4, 0xdc, 0xef, 0x41, 0xe6, 0x3b, 0xc3, 0x43, 0x3e, 0xb5, 0xa2, 0xa0, 0x27, 0xad, 0x9c, 0xc0, 0x57, 0x93, 0x5c, 0x4e, 0xcd, 0x18, 0xea, 0xb0, 0x6b, 0xe3, 0x97, 0x17, 0x3, 0xa3, 0x74, 0x78, 0x6e, 0x8b, 0xa4, 0x61, 0x70, 0x61, 0x61, 0x91, 0xc4, 0x3, 0x78, 0x78, 0x78, 0xa4, 0x61, 0x70, 0x61, 0x70, 0xc4, 0x17, 0x2, 0x20, 0x1, 0x1, 0x26, 0x1, 0x9, 0xfe, 0xfe, 0xff, 0xef, 0x0, 0x0, 0x11, 0x22, 0x33, 0x28, 0x36, 0x1a, 0x0, 0x67, 0x22, 0x43, 0xa4, 0x61, 0x70, 0x67, 0x73, 0x81, 0xa3, 0x6e, 0x62, 0x73, 0x1, 0xa4, 0x61, 0x70, 0x73, 0x75, 0xc4, 0x5, 0x2, 0x20, 0x1, 0x1, 0x22, 0xa3, 0x66, 0x65, 0x65, 0xcd, 0x3, 0xe8, 0xa2, 0x66, 0x76, 0x4, 0xa2, 0x67, 0x68, 0xc4, 0x20, 0x8a, 0xae, 0xf2, 0xee, 0x8f, 0x3, 0x93, 0xb9, 0xa5, 0x47, 0x41, 0x35, 0x3b, 0x97, 0x96, 0xf3, 0xd, 0xcc, 0x52, 0x10, 0x9d, 0x21, 0x15, 0x9a, 0x64, 0xe8, 0x47, 0x52, 0xb2, 0xcc, 0x90, 0x6a, 0xa2, 0x6c, 0x76, 0xcd, 0x3, 0xec, 0xa4, 0x6e, 0x6f, 0x74, 0x65, 0xc4, 0x8, 0x13, 0xfa, 0x3c, 0x55, 0xe8, 0x7b, 0x23, 0xea, 0xa3, 0x73, 0x6e, 0x64, 0xc4, 0x20, 0x4a, 0x82, 0x63, 0xeb, 0xc0, 0xd2, 0xee, 0xed, 0xac, 0x73, 0xdb, 0xb9, 0xd0, 0x27, 0xa1, 0xb2, 0x32, 0x99, 0x7a, 0xed, 0xc5, 0xde, 0xa2, 0x25, 0x7f, 0x7f, 0x2c, 0x8b, 0xcd, 0x42, 0x5f, 0x1a, 0xa4, 0x74, 0x79, 0x70, 0x65, 0xa4, 0x61, 0x70, 0x70, 0x6c},
+			"{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"eHh4\"}}},\"sig\":\"UcqfMsqdZkveoJjZGw0OTTnKAkxOxLqIGrYKY/+wlca2fQC03O9B5jvDQz61oqAnrZzAV5NcTs0Y6rBr45cXAw==\",\"txn\":{\"apaa\":[\"eHh4\"],\"apap\":\"AiABASYBCf7+/+8AABEiMyg2GgBnIkM=\",\"apgs\":{\"nbs\":1},\"apsu\":\"AiABASI=\",\"fee\":1000,\"fv\":4,\"gh\":\"iq7y7o8Dk7mlR0E1O5eW8w3MUhCdIRWaZOhHUrLMkGo=\",\"lv\":1004,\"note\":\"E/o8Veh7I+o=\",\"snd\":\"SoJj68DS7u2sc9u50CehsjKZeu3F3qIlf38si81CXxo=\",\"type\":\"appl\"}}",
+		},
+		{
+			[]byte{0x83, 0xa2, 0x64, 0x74, 0x82, 0xa2, 0x67, 0x64, 0x82, 0xa3, 0x67, 0x6b, 0x62, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0x4, 0x74, 0x65, 0x73, 0x74, 0xa3, 0x67, 0x6b, 0x69, 0x82, 0xa2, 0x61, 0x74, 0x2, 0xa2, 0x75, 0x69, 0x64, 0xa2, 0x6c, 0x64, 0x81, 0x0, 0x82, 0xa3, 0x6c, 0x6b, 0x62, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0xb, 0x61, 0x6e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x74, 0x65, 0x73, 0x74, 0xa3, 0x6c, 0x6b, 0x69, 0x82, 0xa2, 0x61, 0x74, 0x2, 0xa2, 0x75, 0x69, 0xcc, 0xc8, 0xa3, 0x73, 0x69, 0x67, 0xc4, 0x40, 0xc9, 0x25, 0xb2, 0xa, 0x42, 0xda, 0x15, 0xbe, 0x74, 0x16, 0x1d, 0x45, 0xc9, 0x3b, 0xf, 0xa4, 0xcc, 0xdd, 0x86, 0xbd, 0xa, 0x53, 0x1e, 0x43, 0xb3, 0x7e, 0xf9, 0xcc, 0xaf, 0x44, 0x38, 0xce, 0x35, 0xa5, 0xaa, 0xb, 0x96, 0x28, 0x79, 0x6, 0xf8, 0xe1, 0xfb, 0x96, 0xe3, 0x79, 0x9b, 0x27, 0xfa, 0xa4, 0x51, 0x10, 0xc7, 0xb1, 0x84, 0x79, 0x46, 0xf8, 0xd8, 0x6a, 0x6c, 0x96, 0x93, 0x6, 0xa3, 0x74, 0x78, 0x6e, 0x8a, 0xa4, 0x61, 0x70, 0x61, 0x61, 0x91, 0xc4, 0x5, 0x66, 0x69, 0x72, 0x73, 0x74, 0xa4, 0x61, 0x70, 0x61, 0x6e, 0x1, 0xa4, 0x61, 0x70, 0x69, 0x64, 0x23, 0xa3, 0x66, 0x65, 0x65, 0xcd, 0x3, 0xe8, 0xa2, 0x66, 0x76, 0x7, 0xa2, 0x67, 0x68, 0xc4, 0x20, 0x8a, 0xae, 0xf2, 0xee, 0x8f, 0x3, 0x93, 0xb9, 0xa5, 0x47, 0x41, 0x35, 0x3b, 0x97, 0x96, 0xf3, 0xd, 0xcc, 0x52, 0x10, 0x9d, 0x21, 0x15, 0x9a, 0x64, 0xe8, 0x47, 0x52, 0xb2, 0xcc, 0x90, 0x6a, 0xa2, 0x6c, 0x76, 0xcd, 0x3, 0xef, 0xa4, 0x6e, 0x6f, 0x74, 0x65, 0xc4, 0x8, 0xc9, 0x83, 0x5, 0x5f, 0x20, 0x45, 0x8f, 0x98, 0xa3, 0x73, 0x6e, 0x64, 0xc4, 0x20, 0x32, 0xf8, 0xa1, 0x14, 0x66, 0x60, 0x7, 0xb7, 0xfe, 0x8, 0xd2, 0x48, 0x83, 0xdf, 0x28, 0x86, 0x16, 0x74, 0xa3, 0xb2, 0x5, 0x48, 0x1d, 0x4a, 0x45, 0x8e, 0x50, 0xb4, 0xba, 0x2f, 0x34, 0xde, 0xa4, 0x74, 0x79, 0x70, 0x65, 0xa4, 0x61, 0x70, 0x70, 0x6c},
+			"{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"eHh4\"},\"Z2ti\":{\"at\":1,\"bs\":\"dGVzdA==\"},\"Z2tp\":{\"at\":2,\"ui\":100}},\"ld\":{\"0\":{\"bGti\":{\"at\":1,\"bs\":\"YW5vdGhlcnRlc3Q=\"},\"bGtp\":{\"at\":2,\"ui\":200}}}},\"sig\":\"ySWyCkLaFb50Fh1FyTsPpMzdhr0KUx5Ds375zK9EOM41paoLlih5Bvjh+5bjeZsn+qRREMexhHlG+NhqbJaTBg==\",\"txn\":{\"apaa\":[\"Zmlyc3Q=\"],\"apan\":1,\"apap\":\"AiABASYBCf7+/+8AABEiMyg2GgBnIkM=\",\"apgs\":{\"nbs\":1},\"apid\":35,\"apsu\":\"AiABASI=\",\"fee\":1000,\"fv\":7,\"gh\":\"iq7y7o8Dk7mlR0E1O5eW8w3MUhCdIRWaZOhHUrLMkGo=\",\"lv\":1007,\"note\":\"yYMFXyBFj5g=\",\"snd\":\"MvihFGZgB7f+CNJIg98ohhZ0o7IFSB1KRY5QtLovNN4=\",\"type\":\"appl\"}}",
+		},
+	}
+
+	var stxn types.SignedTxnWithAD
+	for i, mt := range testTxns {
+		t.Run(fmt.Sprintf("i=%d", i), func(t *testing.T) {
+			msgpack.Decode(mt.msgpack, &stxn)
+			js := EncodeSignedTxnWithAD(stxn)
+			require.Equal(t, mt.json, string(js))
+		})
+	}
+}
+
+func TestEncodeSignedTxnWithADSynthetic(t *testing.T) {
+	nonutf8b := []byte{254, 254, 255, 239, 0, 0, 17, 34, 51}
+	nonutf8 := string(nonutf8b)
+	var stxn types.SignedTxnWithAD
+	stxn.EvalDelta.GlobalDelta = make(map[string]types.ValueDelta)
+	stxn.EvalDelta.GlobalDelta[nonutf8] = types.ValueDelta{
+		Action: types.SetBytesAction,
+		Bytes:  nonutf8b,
+	}
+	stxn.EvalDelta.LocalDeltas = make(map[uint64]types.StateDelta, 1)
+	ld := make(map[string]types.ValueDelta)
+	ld[nonutf8] = types.ValueDelta{
+		Action: types.SetBytesAction,
+		Bytes:  nonutf8b,
+	}
+	stxn.EvalDelta.LocalDeltas[1] = ld
+	js := EncodeSignedTxnWithAD(stxn)
+	require.Equal(t, "{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"/v7/7wAAESIz\"}},\"ld\":{\"1\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"/v7/7wAAESIz\"}}}}}", string(js))
+}
+
+// Test that encoding to JSON and decoding results in the same object.
+func TestJSONEncoding(t *testing.T) {
+	type Y struct {
+		Num int
+	}
+	type T struct {
+		Num    uint64
+		Str    string
+		Bytes  []byte
+		Object Y
+	}
+
+	x := T{
+		Num:    1,
+		Str:    "abc",
+		Bytes:  []byte{4, 5, 6},
+		Object: Y{Num: 7},
+	}
+	buf := EncodeJSON(x)
+
+	var xx T
+	err := DecodeJSON(buf, &xx)
+	require.NoError(t, err)
+
+	assert.Equal(t, x, xx)
+}
diff --git a/idb/postgres/postgres_integration_test.go b/idb/postgres/postgres_integration_test.go
index 153fe23..43c49af 100644
--- a/idb/postgres/postgres_integration_test.go
+++ b/idb/postgres/postgres_integration_test.go
@@ -11,12 +11,12 @@ import (
 	"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"
 	sdk_types "github.com/algorand/go-algorand-sdk/types"
 
 	"github.com/algorand/indexer/accounting"
 	"github.com/algorand/indexer/idb"
+	"github.com/algorand/indexer/idb/postgres/internal/encoding"
 	"github.com/algorand/indexer/importer"
 	"github.com/algorand/indexer/types"
 	"github.com/algorand/indexer/util/test"
@@ -485,7 +485,7 @@ func TestRekeyBasic(t *testing.T) {
 	assert.NoError(t, err, "querying account data")
 
 	var ad types.AccountData
-	err = json.Decode(accountDataStr, &ad)
+	err = encoding.DecodeJSON(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
 	assert.Equal(t, test.AccountB, ad.SpendingKey)
 }
@@ -534,7 +534,7 @@ func TestRekeyToItself(t *testing.T) {
 	assert.NoError(t, err, "querying account data")
 
 	var ad types.AccountData
-	err = json.Decode(accountDataStr, &ad)
+	err = encoding.DecodeJSON(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
 	assert.Equal(t, sdk_types.ZeroAddress, ad.SpendingKey)
 }
@@ -581,7 +581,7 @@ func TestRekeyThreeTimesInSameRound(t *testing.T) {
 	assert.NoError(t, err, "querying account data")
 
 	var ad types.AccountData
-	err = json.Decode(accountDataStr, &ad)
+	err = encoding.DecodeJSON(accountDataStr, &ad)
 	assert.NoError(t, err, "failed to parse account data json")
 	assert.Equal(t, test.AccountC, ad.SpendingKey)
 }
diff --git a/idb/postgres/postgres_migrations_integration_test.go b/idb/postgres/postgres_migrations_integration_test.go
index 10ec4b8..65fe3ee 100644
--- a/idb/postgres/postgres_migrations_integration_test.go
+++ b/idb/postgres/postgres_migrations_integration_test.go
@@ -6,10 +6,10 @@ import (
 
 	"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/idb/postgres/internal/encoding"
 	"github.com/algorand/indexer/types"
 	"github.com/algorand/indexer/util/test"
 )
@@ -21,7 +21,7 @@ func nextMigrationNum(t *testing.T, db *IndexerDb) int {
 	assert.True(t, len(j) > 0)
 
 	var state MigrationState
-	err = json.Decode([]byte(j), &state)
+	err = encoding.DecodeJSON([]byte(j), &state)
 	assert.NoError(t, err)
 
 	return state.NextMigration
diff --git a/idb/postgres/postgres_test.go b/idb/postgres/postgres_test.go
index 9f582b2..e74cd63 100644
--- a/idb/postgres/postgres_test.go
+++ b/idb/postgres/postgres_test.go
@@ -5,10 +5,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
-
 	"github.com/algorand/indexer/idb"
-	"github.com/algorand/indexer/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -116,50 +113,3 @@ func TestTealKeyValue(t *testing.T) {
 	_, ok = tkv.get(k2)
 	a.False(ok)
 }
-
-type txnMsgpackJSON struct {
-	msgpack []byte
-	json    string
-}
-
-var testTxns = []txnMsgpackJSON{
-	{
-		[]uint8{0x83, 0xa2, 0x64, 0x74, 0x81, 0xa2, 0x67, 0x64, 0x81, 0xa9, 0xfe, 0xfe, 0xff, 0xef, 0x0, 0x0, 0x11, 0x22, 0x33, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0x3, 0x78, 0x78, 0x78, 0xa3, 0x73, 0x69, 0x67, 0xc4, 0x40, 0x51, 0xca, 0x9f, 0x32, 0xca, 0x9d, 0x66, 0x4b, 0xde, 0xa0, 0x98, 0xd9, 0x1b, 0xd, 0xe, 0x4d, 0x39, 0xca, 0x2, 0x4c, 0x4e, 0xc4, 0xba, 0x88, 0x1a, 0xb6, 0xa, 0x63, 0xff, 0xb0, 0x95, 0xc6, 0xb6, 0x7d, 0x0, 0xb4, 0xdc, 0xef, 0x41, 0xe6, 0x3b, 0xc3, 0x43, 0x3e, 0xb5, 0xa2, 0xa0, 0x27, 0xad, 0x9c, 0xc0, 0x57, 0x93, 0x5c, 0x4e, 0xcd, 0x18, 0xea, 0xb0, 0x6b, 0xe3, 0x97, 0x17, 0x3, 0xa3, 0x74, 0x78, 0x6e, 0x8b, 0xa4, 0x61, 0x70, 0x61, 0x61, 0x91, 0xc4, 0x3, 0x78, 0x78, 0x78, 0xa4, 0x61, 0x70, 0x61, 0x70, 0xc4, 0x17, 0x2, 0x20, 0x1, 0x1, 0x26, 0x1, 0x9, 0xfe, 0xfe, 0xff, 0xef, 0x0, 0x0, 0x11, 0x22, 0x33, 0x28, 0x36, 0x1a, 0x0, 0x67, 0x22, 0x43, 0xa4, 0x61, 0x70, 0x67, 0x73, 0x81, 0xa3, 0x6e, 0x62, 0x73, 0x1, 0xa4, 0x61, 0x70, 0x73, 0x75, 0xc4, 0x5, 0x2, 0x20, 0x1, 0x1, 0x22, 0xa3, 0x66, 0x65, 0x65, 0xcd, 0x3, 0xe8, 0xa2, 0x66, 0x76, 0x4, 0xa2, 0x67, 0x68, 0xc4, 0x20, 0x8a, 0xae, 0xf2, 0xee, 0x8f, 0x3, 0x93, 0xb9, 0xa5, 0x47, 0x41, 0x35, 0x3b, 0x97, 0x96, 0xf3, 0xd, 0xcc, 0x52, 0x10, 0x9d, 0x21, 0x15, 0x9a, 0x64, 0xe8, 0x47, 0x52, 0xb2, 0xcc, 0x90, 0x6a, 0xa2, 0x6c, 0x76, 0xcd, 0x3, 0xec, 0xa4, 0x6e, 0x6f, 0x74, 0x65, 0xc4, 0x8, 0x13, 0xfa, 0x3c, 0x55, 0xe8, 0x7b, 0x23, 0xea, 0xa3, 0x73, 0x6e, 0x64, 0xc4, 0x20, 0x4a, 0x82, 0x63, 0xeb, 0xc0, 0xd2, 0xee, 0xed, 0xac, 0x73, 0xdb, 0xb9, 0xd0, 0x27, 0xa1, 0xb2, 0x32, 0x99, 0x7a, 0xed, 0xc5, 0xde, 0xa2, 0x25, 0x7f, 0x7f, 0x2c, 0x8b, 0xcd, 0x42, 0x5f, 0x1a, 0xa4, 0x74, 0x79, 0x70, 0x65, 0xa4, 0x61, 0x70, 0x70, 0x6c},
-		"{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"eHh4\"}}},\"sig\":\"UcqfMsqdZkveoJjZGw0OTTnKAkxOxLqIGrYKY/+wlca2fQC03O9B5jvDQz61oqAnrZzAV5NcTs0Y6rBr45cXAw==\",\"txn\":{\"apaa\":[\"eHh4\"],\"apap\":\"AiABASYBCf7+/+8AABEiMyg2GgBnIkM=\",\"apgs\":{\"nbs\":1},\"apsu\":\"AiABASI=\",\"fee\":1000,\"fv\":4,\"gh\":\"iq7y7o8Dk7mlR0E1O5eW8w3MUhCdIRWaZOhHUrLMkGo=\",\"lv\":1004,\"note\":\"E/o8Veh7I+o=\",\"snd\":\"SoJj68DS7u2sc9u50CehsjKZeu3F3qIlf38si81CXxo=\",\"type\":\"appl\"}}",
-	},
-	{
-		[]byte{0x83, 0xa2, 0x64, 0x74, 0x82, 0xa2, 0x67, 0x64, 0x82, 0xa3, 0x67, 0x6b, 0x62, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0x4, 0x74, 0x65, 0x73, 0x74, 0xa3, 0x67, 0x6b, 0x69, 0x82, 0xa2, 0x61, 0x74, 0x2, 0xa2, 0x75, 0x69, 0x64, 0xa2, 0x6c, 0x64, 0x81, 0x0, 0x82, 0xa3, 0x6c, 0x6b, 0x62, 0x82, 0xa2, 0x61, 0x74, 0x1, 0xa2, 0x62, 0x73, 0xc4, 0xb, 0x61, 0x6e, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x74, 0x65, 0x73, 0x74, 0xa3, 0x6c, 0x6b, 0x69, 0x82, 0xa2, 0x61, 0x74, 0x2, 0xa2, 0x75, 0x69, 0xcc, 0xc8, 0xa3, 0x73, 0x69, 0x67, 0xc4, 0x40, 0xc9, 0x25, 0xb2, 0xa, 0x42, 0xda, 0x15, 0xbe, 0x74, 0x16, 0x1d, 0x45, 0xc9, 0x3b, 0xf, 0xa4, 0xcc, 0xdd, 0x86, 0xbd, 0xa, 0x53, 0x1e, 0x43, 0xb3, 0x7e, 0xf9, 0xcc, 0xaf, 0x44, 0x38, 0xce, 0x35, 0xa5, 0xaa, 0xb, 0x96, 0x28, 0x79, 0x6, 0xf8, 0xe1, 0xfb, 0x96, 0xe3, 0x79, 0x9b, 0x27, 0xfa, 0xa4, 0x51, 0x10, 0xc7, 0xb1, 0x84, 0x79, 0x46, 0xf8, 0xd8, 0x6a, 0x6c, 0x96, 0x93, 0x6, 0xa3, 0x74, 0x78, 0x6e, 0x8a, 0xa4, 0x61, 0x70, 0x61, 0x61, 0x91, 0xc4, 0x5, 0x66, 0x69, 0x72, 0x73, 0x74, 0xa4, 0x61, 0x70, 0x61, 0x6e, 0x1, 0xa4, 0x61, 0x70, 0x69, 0x64, 0x23, 0xa3, 0x66, 0x65, 0x65, 0xcd, 0x3, 0xe8, 0xa2, 0x66, 0x76, 0x7, 0xa2, 0x67, 0x68, 0xc4, 0x20, 0x8a, 0xae, 0xf2, 0xee, 0x8f, 0x3, 0x93, 0xb9, 0xa5, 0x47, 0x41, 0x35, 0x3b, 0x97, 0x96, 0xf3, 0xd, 0xcc, 0x52, 0x10, 0x9d, 0x21, 0x15, 0x9a, 0x64, 0xe8, 0x47, 0x52, 0xb2, 0xcc, 0x90, 0x6a, 0xa2, 0x6c, 0x76, 0xcd, 0x3, 0xef, 0xa4, 0x6e, 0x6f, 0x74, 0x65, 0xc4, 0x8, 0xc9, 0x83, 0x5, 0x5f, 0x20, 0x45, 0x8f, 0x98, 0xa3, 0x73, 0x6e, 0x64, 0xc4, 0x20, 0x32, 0xf8, 0xa1, 0x14, 0x66, 0x60, 0x7, 0xb7, 0xfe, 0x8, 0xd2, 0x48, 0x83, 0xdf, 0x28, 0x86, 0x16, 0x74, 0xa3, 0xb2, 0x5, 0x48, 0x1d, 0x4a, 0x45, 0x8e, 0x50, 0xb4, 0xba, 0x2f, 0x34, 0xde, 0xa4, 0x74, 0x79, 0x70, 0x65, 0xa4, 0x61, 0x70, 0x70, 0x6c},
-		"{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"eHh4\"},\"Z2ti\":{\"at\":1,\"bs\":\"dGVzdA==\"},\"Z2tp\":{\"at\":2,\"ui\":100}},\"ld\":{\"0\":{\"bGti\":{\"at\":1,\"bs\":\"YW5vdGhlcnRlc3Q=\"},\"bGtp\":{\"at\":2,\"ui\":200}}}},\"sig\":\"ySWyCkLaFb50Fh1FyTsPpMzdhr0KUx5Ds375zK9EOM41paoLlih5Bvjh+5bjeZsn+qRREMexhHlG+NhqbJaTBg==\",\"txn\":{\"apaa\":[\"Zmlyc3Q=\"],\"apan\":1,\"apap\":\"AiABASYBCf7+/+8AABEiMyg2GgBnIkM=\",\"apgs\":{\"nbs\":1},\"apid\":35,\"apsu\":\"AiABASI=\",\"fee\":1000,\"fv\":7,\"gh\":\"iq7y7o8Dk7mlR0E1O5eW8w3MUhCdIRWaZOhHUrLMkGo=\",\"lv\":1007,\"note\":\"yYMFXyBFj5g=\",\"snd\":\"MvihFGZgB7f+CNJIg98ohhZ0o7IFSB1KRY5QtLovNN4=\",\"type\":\"appl\"}}",
-	},
-}
-
-func TestStxnJSON(t *testing.T) {
-	var stxn types.SignedTxnWithAD
-	for i, mt := range testTxns {
-		t.Run(fmt.Sprintf("i=%d", i), func(t *testing.T) {
-			msgpack.Decode(mt.msgpack, &stxn)
-			js := stxnToJSON(stxn)
-			require.Equal(t, mt.json, string(js))
-		})
-	}
-}
-
-func TestStxnJSONSynthetic(t *testing.T) {
-	nonutf8b := []byte{254, 254, 255, 239, 0, 0, 17, 34, 51}
-	nonutf8 := string(nonutf8b)
-	var stxn types.SignedTxnWithAD
-	stxn.EvalDelta.GlobalDelta = make(map[string]types.ValueDelta)
-	stxn.EvalDelta.GlobalDelta[nonutf8] = types.ValueDelta{
-		Action: types.SetBytesAction,
-		Bytes:  nonutf8b,
-	}
-	stxn.EvalDelta.LocalDeltas = make(map[uint64]types.StateDelta, 1)
-	ld := make(map[string]types.ValueDelta)
-	ld[nonutf8] = types.ValueDelta{
-		Action: types.SetBytesAction,
-		Bytes:  nonutf8b,
-	}
-	stxn.EvalDelta.LocalDeltas[1] = ld
-	js := stxnToJSON(stxn)
-	require.Equal(t, "{\"dt\":{\"gd\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"/v7/7wAAESIz\"}},\"ld\":{\"1\":{\"/v7/7wAAESIz\":{\"at\":1,\"bs\":\"/v7/7wAAESIz\"}}}}}", string(js))
-}
diff --git a/util/test/testutil.go b/util/test/testutil.go
index 971cbbc..aefda8e 100644
--- a/util/test/testutil.go
+++ b/util/test/testutil.go
@@ -107,7 +107,7 @@ func PrintTxnQuery(db idb.IndexerDb, q idb.TransactionFilter) {
 		//tjs, err := json.Marshal(stxn.Txn) // nope, ugly
 		//MaybeFail(err, "err %v\n", err)
 		//tjs := string(JSONOneLine(stxn.Txn))
-		tjs := string(idb.JSONOneLine(stxn.Txn))
+		tjs := string(util.JSONOneLine(stxn.Txn))
 		info("%d:%d %s sr=%d rr=%d ca=%d cr=%d t=%s\n", txnrow.Round, txnrow.Intra, tjs, stxn.SenderRewards, stxn.ReceiverRewards, stxn.ClosingAmount, stxn.CloseRewards, txnrow.RoundTime.String())
 		count++
 	}