Repository: coinbase/rosetta-sdk-go
Pull request #84 (2020-07-25 23:35:59)

v1.4.1 Specification Support
This PR adds support for the `v1.4.1` release of the Rosetta Specification: https://github.com/coinbase/rosetta-specifications/releases/tag/v1.4.1

### Changes
- [x] Add Coin assertions
- [x] Return Coins in account balance response


Patch:
diff --git a/asserter/account.go b/asserter/account.go
index a72ed0d..67cd8d2 100644
--- a/asserter/account.go
+++ b/asserter/account.go
@@ -63,34 +63,39 @@ func assertBalanceAmounts(amounts []*types.Amount) error {
 // if the same currency is present in multiple amounts.
 func AccountBalanceResponse(
 	requestBlock *types.PartialBlockIdentifier,
-	responseBlock *types.BlockIdentifier,
-	balances []*types.Amount,
+	response *types.AccountBalanceResponse,
 ) error {
-	if err := BlockIdentifier(responseBlock); err != nil {
-		return err
+	if err := BlockIdentifier(response.BlockIdentifier); err != nil {
+		return fmt.Errorf("%w: block identifier is invalid", err)
 	}
 
-	if err := assertBalanceAmounts(balances); err != nil {
-		return err
+	if err := assertBalanceAmounts(response.Balances); err != nil {
+		return fmt.Errorf("%w: balance amounts are invalid", err)
+	}
+
+	if response.Coins != nil {
+		if err := Coins(response.Coins); err != nil {
+			return fmt.Errorf("%w: coins are invalid", err)
+		}
 	}
 
 	if requestBlock == nil {
 		return nil
 	}
 
-	if requestBlock.Hash != nil && *requestBlock.Hash != responseBlock.Hash {
+	if requestBlock.Hash != nil && *requestBlock.Hash != response.BlockIdentifier.Hash {
 		return fmt.Errorf(
 			"request block hash %s does not match response block hash %s",
 			*requestBlock.Hash,
-			responseBlock.Hash,
+			response.BlockIdentifier.Hash,
 		)
 	}
 
-	if requestBlock.Index != nil && *requestBlock.Index != responseBlock.Index {
+	if requestBlock.Index != nil && *requestBlock.Index != response.BlockIdentifier.Index {
 		return fmt.Errorf(
 			"request block index %d does not match response block index %d",
 			*requestBlock.Index,
-			responseBlock.Index,
+			response.BlockIdentifier.Index,
 		)
 	}
 
diff --git a/asserter/block.go b/asserter/block.go
index ee74c55..5c9d0ef 100644
--- a/asserter/block.go
+++ b/asserter/block.go
@@ -179,11 +179,11 @@ func (a *Asserter) Operation(
 	}
 
 	if err := OperationIdentifier(operation.OperationIdentifier, index); err != nil {
-		return err
+		return fmt.Errorf("%w: operation identifier is invalid in operation %d", err, index)
 	}
 
 	if err := a.OperationType(operation.Type); err != nil {
-		return err
+		return fmt.Errorf("%w: operation type is invalid in operation %d", err, index)
 	}
 
 	if construction && len(operation.Status) != 0 {
@@ -191,7 +191,7 @@ func (a *Asserter) Operation(
 	}
 
 	if err := a.OperationStatus(operation.Status); err != nil && !construction {
-		return err
+		return fmt.Errorf("%w: operation status is invalid in operation %d", err, index)
 	}
 
 	if operation.Amount == nil {
@@ -199,11 +199,19 @@ func (a *Asserter) Operation(
 	}
 
 	if err := AccountIdentifier(operation.Account); err != nil {
-		return err
+		return fmt.Errorf("%w: account identifier is invalid in operation %d", err, index)
 	}
 
 	if err := Amount(operation.Amount); err != nil {
-		return err
+		return fmt.Errorf("%w: amount is invalid in operation %d", err, index)
+	}
+
+	if operation.CoinChange == nil {
+		return nil
+	}
+
+	if err := CoinChange(operation.CoinChange); err != nil {
+		return fmt.Errorf("%w: coin change is invalid in operation %d", err, index)
 	}
 
 	return nil
diff --git a/asserter/coin.go b/asserter/coin.go
new file mode 100644
index 0000000..1dbedf0
--- /dev/null
+++ b/asserter/coin.go
@@ -0,0 +1,107 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package asserter
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/coinbase/rosetta-sdk-go/types"
+)
+
+// Coin returns an error if the provided *types.Coin is invalid.
+func Coin(coin *types.Coin) error {
+	if coin == nil {
+		return errors.New("coin cannot be nil")
+	}
+
+	if err := CoinIdentifier(coin.CoinIdentifier); err != nil {
+		return fmt.Errorf("%w: coin identifier is invalid", err)
+	}
+
+	if err := Amount(coin.Amount); err != nil {
+		return fmt.Errorf("%w: coin amount is invalid", err)
+	}
+
+	return nil
+}
+
+// Coins returns an error if the provided
+// []*types.Coin is invalid. If there are any
+// duplicate identifiers, this function
+// will also return an error.
+func Coins(coins []*types.Coin) error {
+	ids := map[string]struct{}{}
+	for _, coin := range coins {
+		if err := Coin(coin); err != nil {
+			return fmt.Errorf("%w: coin is invalid", err)
+		}
+
+		if _, exists := ids[coin.CoinIdentifier.Identifier]; exists {
+			return fmt.Errorf(
+				"duplicate coin identifier detected: %s",
+				coin.CoinIdentifier.Identifier,
+			)
+		}
+
+		ids[coin.CoinIdentifier.Identifier] = struct{}{}
+	}
+
+	return nil
+}
+
+// CoinIdentifier returns an error if the provided *types.CoinIdentifier
+// is invalid.
+func CoinIdentifier(coinIdentifier *types.CoinIdentifier) error {
+	if coinIdentifier == nil {
+		return errors.New("coin identifier cannot be nil")
+	}
+
+	if len(coinIdentifier.Identifier) == 0 {
+		return errors.New("coin identifier cannot be empty")
+	}
+
+	return nil
+}
+
+// CoinChange returns an error if the provided *types.CoinChange
+// is invalid.
+func CoinChange(change *types.CoinChange) error {
+	if change == nil {
+		return errors.New("coin change cannot be nil")
+	}
+
+	if err := CoinIdentifier(change.CoinIdentifier); err != nil {
+		return fmt.Errorf("%w: coin identifier is invalid", err)
+	}
+
+	if err := CoinAction(change.CoinAction); err != nil {
+		return fmt.Errorf("%w: coin action is invalid", err)
+	}
+
+	return nil
+}
+
+// CoinAction returns an error if the provided types.CoinAction
+// is invalid.
+func CoinAction(action types.CoinAction) error {
+	switch action {
+	case types.CoinCreated, types.CoinSpent:
+	default:
+		return fmt.Errorf("%s is not a valid coin action", action)
+	}
+
+	return nil
+}
diff --git a/asserter/construction.go b/asserter/construction.go
index 3f0a3ed..6493ae1 100644
--- a/asserter/construction.go
+++ b/asserter/construction.go
@@ -37,12 +37,11 @@ func ConstructionMetadataResponse(
 	return nil
 }
 
-// ConstructionSubmitResponse returns an error if
+// TransactionIdentifierResponse returns an error if
 // the types.TransactionIdentifier in the response is not
-// valid or if the Submission.Status is not contained
-// within the provided validStatuses slice.
-func ConstructionSubmitResponse(
-	response *types.ConstructionSubmitResponse,
+// valid.
+func TransactionIdentifierResponse(
+	response *types.TransactionIdentifierResponse,
 ) error {
 	if response == nil {
 		return errors.New("construction submit response cannot be nil")
@@ -89,23 +88,6 @@ func ConstructionDeriveResponse(
 	return nil
 }
 
-// ConstructionHashResponse returns an error if
-// a *types.ConstructionHashResponse does
-// not have a populated transaction hash.
-func ConstructionHashResponse(
-	response *types.ConstructionHashResponse,
-) error {
-	if response == nil {
-		return errors.New("construction hash response cannot be nil")
-	}
-
-	if len(response.TransactionHash) == 0 {
-		return errors.New("transaction hash cannot be empty")
-	}
-
-	return nil
-}
-
 // ConstructionParseResponse returns an error if
 // a *types.ConstructionParseResponse does
 // not have a valid set of operations or
diff --git a/client/api_block.go b/client/api_block.go
index cce2583..d2c8da0 100644
--- a/client/api_block.go
+++ b/client/api_block.go
@@ -36,7 +36,11 @@ type BlockAPIService service
 // Block Get a block by its Block Identifier. If transactions are returned in the same call to the
 // node as fetching the block, the response should include these transactions in the Block object.
 // If not, an array of Transaction Identifiers should be returned so /block/transaction fetches can
-// be done to get all transaction information.
+// be done to get all transaction information. When requesting a block by the hash component of the
+// BlockIdentifier, this request MUST be idempotent: repeated invocations for the same
+// hash-identified block must return the exact same block contents. No such restriction is imposed
+// when requesting a block by height, given that a chain reorg event might cause the specific block
+// at height n to be set to a different one.
 func (a *BlockAPIService) Block(
 	ctx _context.Context,
 	blockRequest *types.BlockRequest,
diff --git a/client/api_construction.go b/client/api_construction.go
index f8f1f4e..9749d7e 100644
--- a/client/api_construction.go
+++ b/client/api_construction.go
@@ -178,7 +178,7 @@ func (a *ConstructionAPIService) ConstructionDerive(
 func (a *ConstructionAPIService) ConstructionHash(
 	ctx _context.Context,
 	constructionHashRequest *types.ConstructionHashRequest,
-) (*types.ConstructionHashResponse, *types.Error, error) {
+) (*types.TransactionIdentifierResponse, *types.Error, error) {
 	var (
 		localVarPostBody interface{}
 	)
@@ -233,7 +233,7 @@ func (a *ConstructionAPIService) ConstructionHash(
 		return nil, &v, fmt.Errorf("%+v", v)
 	}
 
-	var v types.ConstructionHashResponse
+	var v types.TransactionIdentifierResponse
 	err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
 	if err != nil {
 		return nil, nil, err
@@ -540,7 +540,7 @@ func (a *ConstructionAPIService) ConstructionPreprocess(
 func (a *ConstructionAPIService) ConstructionSubmit(
 	ctx _context.Context,
 	constructionSubmitRequest *types.ConstructionSubmitRequest,
-) (*types.ConstructionSubmitResponse, *types.Error, error) {
+) (*types.TransactionIdentifierResponse, *types.Error, error) {
 	var (
 		localVarPostBody interface{}
 	)
@@ -595,7 +595,7 @@ func (a *ConstructionAPIService) ConstructionSubmit(
 		return nil, &v, fmt.Errorf("%+v", v)
 	}
 
-	var v types.ConstructionSubmitResponse
+	var v types.TransactionIdentifierResponse
 	err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
 	if err != nil {
 		return nil, nil, err
diff --git a/client/client.go b/client/client.go
index 7795a05..c80e751 100644
--- a/client/client.go
+++ b/client/client.go
@@ -36,7 +36,7 @@ var (
 	jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`)
 )
 
-// APIClient manages communication with the Rosetta API v1.4.0
+// APIClient manages communication with the Rosetta API v1.4.1
 // In most cases there should be only one, shared, APIClient.
 type APIClient struct {
 	cfg    *Configuration
diff --git a/codegen.sh b/codegen.sh
index 6598fe2..e9dd05b 100755
--- a/codegen.sh
+++ b/codegen.sh
@@ -53,7 +53,7 @@ done
 rm -rf tmp;
 
 # Download spec file from releases
-ROSETTA_SPEC_VERSION=v1.4.0
+ROSETTA_SPEC_VERSION=v1.4.1
 curl -L https://github.com/coinbase/rosetta-specifications/releases/download/${ROSETTA_SPEC_VERSION}/api.json -o api.json;
 
 # Generate client + types code
@@ -117,13 +117,16 @@ sed "${SED_IFLAG[@]}" 's/Url/URL/g' client/* server/*;
 # Fix enum pointers
 sed "${SED_IFLAG[@]}" 's/*CurveType/CurveType/g' client/* server/*;
 sed "${SED_IFLAG[@]}" 's/*SignatureType/SignatureType/g' client/* server/*;
+sed "${SED_IFLAG[@]}" 's/*CoinAction/CoinAction/g' client/* server/*;
 
-# Fix CurveTypes and SignatureTypes
+# Fix CurveTypes, SignatureTypes, and CoinActions
 sed "${SED_IFLAG[@]}" 's/SECP256K1/Secp256k1/g' client/* server/*;
 sed "${SED_IFLAG[@]}" 's/EDWARDS25519/Edwards25519/g' client/* server/*;
 sed "${SED_IFLAG[@]}" 's/ECDSA_RECOVERY/EcdsaRecovery/g' client/* server/*;
 sed "${SED_IFLAG[@]}" 's/ECDSA/Ecdsa/g' client/* server/*;
 sed "${SED_IFLAG[@]}" 's/ED25519/Ed25519/g' client/* server/*;
+sed "${SED_IFLAG[@]}" 's/CREATED/CoinCreated/g' client/* server/*;
+sed "${SED_IFLAG[@]}" 's/SPENT/CoinSpent/g' client/* server/*;
 
 # Convert HexBytes to Bytes
 sed "${SED_IFLAG[@]}" '/Hex-encoded public key bytes in the format specified by the CurveType/d' client/* server/*;
diff --git a/fetcher/account.go b/fetcher/account.go
index d7a7470..911a847 100644
--- a/fetcher/account.go
+++ b/fetcher/account.go
@@ -31,7 +31,7 @@ func (f *Fetcher) AccountBalance(
 	network *types.NetworkIdentifier,
 	account *types.AccountIdentifier,
 	block *types.PartialBlockIdentifier,
-) (*types.BlockIdentifier, []*types.Amount, map[string]interface{}, error) {
+) (*types.BlockIdentifier, []*types.Amount, []*types.Coin, map[string]interface{}, error) {
 	response, _, err := f.rosettaClient.AccountAPI.AccountBalance(ctx,
 		&types.AccountBalanceRequest{
 			NetworkIdentifier: network,
@@ -40,20 +40,17 @@ func (f *Fetcher) AccountBalance(
 		},
 	)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
-	responseBlock := response.BlockIdentifier
-	balances := response.Balances
 	if err := asserter.AccountBalanceResponse(
 		block,
-		responseBlock,
-		balances,
+		response,
 	); err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
-	return responseBlock, balances, response.Metadata, nil
+	return response.BlockIdentifier, response.Balances, response.Coins, response.Metadata, nil
 }
 
 // AccountBalanceRetry retrieves the validated AccountBalance
@@ -64,25 +61,25 @@ func (f *Fetcher) AccountBalanceRetry(
 	network *types.NetworkIdentifier,
 	account *types.AccountIdentifier,
 	block *types.PartialBlockIdentifier,
-) (*types.BlockIdentifier, []*types.Amount, map[string]interface{}, error) {
+) (*types.BlockIdentifier, []*types.Amount, []*types.Coin, map[string]interface{}, error) {
 	backoffRetries := backoffRetries(
 		f.retryElapsedTime,
 		f.maxRetries,
 	)
 
 	for {
-		responseBlock, balances, metadata, err := f.AccountBalance(
+		responseBlock, balances, coins, metadata, err := f.AccountBalance(
 			ctx,
 			network,
 			account,
 			block,
 		)
 		if err == nil {
-			return responseBlock, balances, metadata, nil
+			return responseBlock, balances, coins, metadata, nil
 		}
 
 		if ctx.Err() != nil {
-			return nil, nil, nil, ctx.Err()
+			return nil, nil, nil, nil, ctx.Err()
 		}
 
 		if !tryAgain(
@@ -94,7 +91,7 @@ func (f *Fetcher) AccountBalanceRetry(
 		}
 	}
 
-	return nil, nil, nil, fmt.Errorf(
+	return nil, nil, nil, nil, fmt.Errorf(
 		"%w: unable to fetch account %s",
 		ErrExhaustedRetries,
 		types.PrettyPrintStruct(account),
diff --git a/fetcher/construction.go b/fetcher/construction.go
index a0259ed..b563728 100644
--- a/fetcher/construction.go
+++ b/fetcher/construction.go
@@ -86,7 +86,7 @@ func (f *Fetcher) ConstructionHash(
 	ctx context.Context,
 	network *types.NetworkIdentifier,
 	signedTransaction string,
-) (string, error) {
+) (*types.TransactionIdentifier, error) {
 	response, _, err := f.rosettaClient.ConstructionAPI.ConstructionHash(ctx,
 		&types.ConstructionHashRequest{
 			NetworkIdentifier: network,
@@ -94,14 +94,14 @@ func (f *Fetcher) ConstructionHash(
 		},
 	)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	if err := asserter.ConstructionHashResponse(response); err != nil {
-		return "", err
+	if err := asserter.TransactionIdentifierResponse(response); err != nil {
+		return nil, err
 	}
 
-	return response.TransactionHash, nil
+	return response.TransactionIdentifier, nil
 }
 
 // ConstructionMetadata returns the validated response
@@ -240,7 +240,7 @@ func (f *Fetcher) ConstructionSubmit(
 		return nil, nil, err
 	}
 
-	if err := asserter.ConstructionSubmitResponse(submitResponse); err != nil {
+	if err := asserter.TransactionIdentifierResponse(submitResponse); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/parser/intent.go b/parser/intent.go
index 88df89a..51d5e34 100644
--- a/parser/intent.go
+++ b/parser/intent.go
@@ -15,7 +15,6 @@
 package parser
 
 import (
-	"errors"
 	"fmt"
 	"log"
 
@@ -58,16 +57,13 @@ func ExpectedOperation(intent *types.Operation, observed *types.Operation) error
 // ExpectedOperations returns an error if a slice of intended
 // operations differ from observed operations. Optionally,
 // it is possible to error if any extra observed opertions
-// are found or if operations matched are not considered
-// successful.
-func (p *Parser) ExpectedOperations(
+// are found.
+func ExpectedOperations(
 	intent []*types.Operation,
 	observed []*types.Operation,
 	errExtra bool,
-	confirmSuccess bool,
 ) error {
 	matches := make(map[int]struct{})
-	failedMatches := []*types.Operation{}
 	unmatched := []*types.Operation{}
 
 	for _, obs := range observed {
@@ -77,28 +73,11 @@ func (p *Parser) ExpectedOperations(
 				continue
 			}
 
-			// Any error returned here only indicated that intent
-			// does not match observed. For ExpectedOperations,
-			// we don't care about the content of the error, we
-			// just care if it errors so we can evaluate the next
-			// operation for a match.
-			if err := ExpectedOperation(in, obs); err != nil {
-				continue
-			}
-
-			obsSuccess, err := p.Asserter.OperationSuccessful(obs)
-			if err != nil {
-				return fmt.Errorf("%w: unable to check operation success", err)
+			if ExpectedOperation(in, obs) == nil {
+				matches[i] = struct{}{}
+				foundMatch = true
+				break
 			}
-
-			if confirmSuccess && !obsSuccess {
-				failedMatches = append(failedMatches, obs)
-				continue
-			}
-
-			matches[i] = struct{}{}
-			foundMatch = true
-			break
 		}
 
 		if !foundMatch {
@@ -112,28 +91,13 @@ func (p *Parser) ExpectedOperations(
 		}
 	}
 
-	missingIntent := []int{}
 	for i := 0; i < len(intent); i++ {
 		if _, exists := matches[i]; !exists {
-			missingIntent = append(missingIntent, i)
-		}
-	}
-
-	if len(missingIntent) > 0 {
-		errString := fmt.Sprintf(
-			"could intent match for %v",
-			missingIntent,
-		)
-
-		if len(failedMatches) > 0 {
-			errString += fmt.Sprintf(
-				"%s: found matching ops with unsuccessful status %s",
-				errString,
-				types.PrettyPrintStruct(failedMatches),
+			return fmt.Errorf(
+				"could not find operation with intent %s in observed",
+				types.PrettyPrintStruct(intent[i]),
 			)
 		}
-
-		return errors.New(errString)
 	}
 
 	if len(unmatched) > 0 {
diff --git a/reconciler/reconciler.go b/reconciler/reconciler.go
index 399a2f6..6692618 100644
--- a/reconciler/reconciler.go
+++ b/reconciler/reconciler.go
@@ -769,7 +769,7 @@ func GetCurrencyBalance(
 	currency *types.Currency,
 	block *types.PartialBlockIdentifier,
 ) (*types.BlockIdentifier, string, error) {
-	liveBlock, liveBalances, _, err := fetcher.AccountBalanceRetry(
+	liveBlock, liveBalances, _, _, err := fetcher.AccountBalanceRetry(
 		ctx,
 		network,
 		account,
diff --git a/server/api.go b/server/api.go
index 7000c90..0085ba9 100644
--- a/server/api.go
+++ b/server/api.go
@@ -119,7 +119,7 @@ type ConstructionAPIServicer interface {
 	ConstructionHash(
 		context.Context,
 		*types.ConstructionHashRequest,
-	) (*types.ConstructionHashResponse, *types.Error)
+	) (*types.TransactionIdentifierResponse, *types.Error)
 	ConstructionMetadata(
 		context.Context,
 		*types.ConstructionMetadataRequest,
@@ -139,7 +139,7 @@ type ConstructionAPIServicer interface {
 	ConstructionSubmit(
 		context.Context,
 		*types.ConstructionSubmitRequest,
-	) (*types.ConstructionSubmitResponse, *types.Error)
+	) (*types.TransactionIdentifierResponse, *types.Error)
 }
 
 // MempoolAPIServicer defines the api actions for the MempoolAPI service
diff --git a/types/account_balance_response.go b/types/account_balance_response.go
index d5dd338..9578b6f 100644
--- a/types/account_balance_response.go
+++ b/types/account_balance_response.go
@@ -23,6 +23,11 @@ type AccountBalanceResponse struct {
 	BlockIdentifier *BlockIdentifier `json:"block_identifier"`
 	// A single account may have a balance in multiple currencies.
 	Balances []*Amount `json:"balances"`
+	// If a blockchain is UTXO-based, all unspent Coins owned by an account_identifier should be
+	// returned alongside the balance. It is highly recommended to populate this field so that users
+	// of the Rosetta API implementation don't need to maintain their own indexer to track their
+	// UTXOs.
+	Coins []*Coin `json:"coins,omitempty"`
 	// Account-based blockchains that utilize a nonce or sequence number should include that number
 	// in the metadata. This number could be unique to the identifier or global across the account
 	// address.
diff --git a/types/block.go b/types/block.go
index 95dfb0c..9cb7d8c 100644
--- a/types/block.go
+++ b/types/block.go
@@ -16,7 +16,11 @@
 
 package types
 
-// Block Blocks contain an array of Transactions that occurred at a particular BlockIdentifier.
+// Block Blocks contain an array of Transactions that occurred at a particular BlockIdentifier. A
+// hard requirement for blocks returned by Rosetta implementations is that they MUST be
+// _inalterable_: once a client has requested and received a block identified by a specific
+// BlockIndentifier, all future calls for that same BlockIdentifier must return the same block
+// contents.
 type Block struct {
 	BlockIdentifier       *BlockIdentifier `json:"block_identifier"`
 	ParentBlockIdentifier *BlockIdentifier `json:"parent_block_identifier"`
diff --git a/types/construction_hash_response.go b/types/coin.go
similarity index 77%
rename from types/construction_hash_response.go
rename to types/coin.go
index 6eee417..26762c5 100644
--- a/types/construction_hash_response.go
+++ b/types/coin.go
@@ -16,8 +16,8 @@
 
 package types
 
-// ConstructionHashResponse ConstructionHashResponse is the output of the `/construction/hash`
-// endpoint.
-type ConstructionHashResponse struct {
-	TransactionHash string `json:"transaction_hash"`
+// Coin Coin contains its unique identifier and the amount it represents.
+type Coin struct {
+	CoinIdentifier *CoinIdentifier `json:"coin_identifier"`
+	Amount         *Amount         `json:"amount"`
 }
diff --git a/types/coin_action.go b/types/coin_action.go
new file mode 100644
index 0000000..1850428
--- /dev/null
+++ b/types/coin_action.go
@@ -0,0 +1,28 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Generated by: OpenAPI Generator (https://openapi-generator.tech)
+
+package types
+
+// CoinAction CoinActions are different state changes that a Coin can undergo. When a Coin is
+// created, it is coin_created. When a Coin is spent, it is coin_spent. It is assumed that a single
+// Coin cannot be created or spent more than once.
+type CoinAction string
+
+// List of CoinAction
+const (
+	CoinCreated CoinAction = "coin_created"
+	CoinSpent   CoinAction = "coin_spent"
+)
diff --git a/types/coin_change.go b/types/coin_change.go
new file mode 100644
index 0000000..71385c6
--- /dev/null
+++ b/types/coin_change.go
@@ -0,0 +1,27 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Generated by: OpenAPI Generator (https://openapi-generator.tech)
+
+package types
+
+// CoinChange CoinChange is used to represent a change in state of a some coin identified by a
+// coin_identifier. This object is part of the Operation model and must be populated for UTXO-based
+// blockchains. Coincidentally, this abstraction of UTXOs allows for supporting both account-based
+// transfers and UTXO-based transfers on the same blockchain (when a transfer is account-based,
+// don't populate this model).
+type CoinChange struct {
+	CoinIdentifier *CoinIdentifier `json:"coin_identifier"`
+	CoinAction     CoinAction      `json:"coin_action"`
+}
diff --git a/types/coin_identifier.go b/types/coin_identifier.go
new file mode 100644
index 0000000..532d084
--- /dev/null
+++ b/types/coin_identifier.go
@@ -0,0 +1,24 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Generated by: OpenAPI Generator (https://openapi-generator.tech)
+
+package types
+
+// CoinIdentifier CoinIdentifier uniquely identifies a Coin.
+type CoinIdentifier struct {
+	// Identifier should be populated with a globally unique identifier of a Coin. In Bitcoin, this
+	// identifier would be transaction_hash:index.
+	Identifier string `json:"identifier"`
+}
diff --git a/types/error.go b/types/error.go
index 4bf5c3e..1057b33 100644
--- a/types/error.go
+++ b/types/error.go
@@ -17,12 +17,16 @@
 package types
 
 // Error Instead of utilizing HTTP status codes to describe node errors (which often do not have a
-// good analog), rich errors are returned using this object.
+// good analog), rich errors are returned using this object. Both the code and message fields can be
+// individually used to correctly identify an error. Implementations MUST use unique values for both
+// fields.
 type Error struct {
 	// Code is a network-specific error code. If desired, this code can be equivalent to an HTTP
 	// status code.
 	Code int32 `json:"code"`
-	// Message is a network-specific error message.
+	// Message is a network-specific error message. The message MUST NOT change for a given code. In
+	// particular, this means that any contextual information should be included in the details
+	// field.
 	Message string `json:"message"`
 	// An error is retriable if the same request may succeed if submitted again.
 	Retriable bool `json:"retriable"`
diff --git a/types/network_status_response.go b/types/network_status_response.go
index 4d3009b..d0d0104 100644
--- a/types/network_status_response.go
+++ b/types/network_status_response.go
@@ -17,9 +17,14 @@
 package types
 
 // NetworkStatusResponse NetworkStatusResponse contains basic information about the node's view of a
-// blockchain network. If a Rosetta implementation prunes historical state, it should populate the
-// optional `oldest_block_identifier` field with the oldest block available to query. If this is not
-// populated, it is assumed that the `genesis_block_identifier` is the oldest queryable block.
+// blockchain network. It is assumed that any BlockIdentifier.Index less than or equal to
+// CurrentBlockIdentifier.Index can be queried. If a Rosetta implementation prunes historical state,
+// it should populate the optional `oldest_block_identifier` field with the oldest block available
+// to query. If this is not populated, it is assumed that the `genesis_block_identifier` is the
+// oldest queryable block. If a Rosetta implementation performs some pre-sync before it is possible
+// to query blocks, sync_status should be populated so that clients can still monitor healthiness.
+// Without this field, it may appear that the implementation is stuck syncing and needs to be
+// terminated.
 type NetworkStatusResponse struct {
 	CurrentBlockIdentifier *BlockIdentifier `json:"current_block_identifier"`
 	// The timestamp of the block in milliseconds since the Unix Epoch. The timestamp is stored in
@@ -27,5 +32,6 @@ type NetworkStatusResponse struct {
 	CurrentBlockTimestamp  int64            `json:"current_block_timestamp"`
 	GenesisBlockIdentifier *BlockIdentifier `json:"genesis_block_identifier"`
 	OldestBlockIdentifier  *BlockIdentifier `json:"oldest_block_identifier,omitempty"`
+	SyncStatus             *SyncStatus      `json:"sync_status,omitempty"`
 	Peers                  []*Peer          `json:"peers"`
 }
diff --git a/types/operation.go b/types/operation.go
index f6a065b..9c5436e 100644
--- a/types/operation.go
+++ b/types/operation.go
@@ -34,8 +34,9 @@ type Operation struct {
 	// because blockchains with smart contracts may have transactions that partially apply.
 	// Blockchains with atomic transactions (all operations succeed or all operations fail) will
 	// have the same status for each operation.
-	Status   string                 `json:"status"`
-	Account  *AccountIdentifier     `json:"account,omitempty"`
-	Amount   *Amount                `json:"amount,omitempty"`
-	Metadata map[string]interface{} `json:"metadata,omitempty"`
+	Status     string                 `json:"status"`
+	Account    *AccountIdentifier     `json:"account,omitempty"`
+	Amount     *Amount                `json:"amount,omitempty"`
+	CoinChange *CoinChange            `json:"coin_change,omitempty"`
+	Metadata   map[string]interface{} `json:"metadata,omitempty"`
 }
diff --git a/types/sync_status.go b/types/sync_status.go
new file mode 100644
index 0000000..145c5aa
--- /dev/null
+++ b/types/sync_status.go
@@ -0,0 +1,31 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Generated by: OpenAPI Generator (https://openapi-generator.tech)
+
+package types
+
+// SyncStatus SyncStatus is used to provide additional context about an implementation's sync
+// status. It is often used to indicate that an implementation is healthy when it cannot be queried
+// until some sync phase occurs. If an implementation is immediately queryable, this model is often
+// not populated.
+type SyncStatus struct {
+	// CurrentIndex is the index of the last synced block in the current stage.
+	CurrentIndex int64 `json:"current_index"`
+	// TargetIndex is the index of the block that the implementation is attempting to sync to in the
+	// current stage.
+	TargetIndex *int64 `json:"target_index,omitempty"`
+	// Stage is the phase of the sync process.
+	Stage *string `json:"stage,omitempty"`
+}
diff --git a/types/construction_submit_response.go b/types/transaction_identifier_response.go
similarity index 77%
rename from types/construction_submit_response.go
rename to types/transaction_identifier_response.go
index ff3243b..1ddc59f 100644
--- a/types/construction_submit_response.go
+++ b/types/transaction_identifier_response.go
@@ -16,9 +16,9 @@
 
 package types
 
-// ConstructionSubmitResponse A TransactionSubmitResponse contains the transaction_identifier of a
-// submitted transaction that was accepted into the mempool.
-type ConstructionSubmitResponse struct {
+// TransactionIdentifierResponse TransactionIdentifierResponse contains the transaction_identifier
+// of a transaction that was submitted to either `/construction/hash` or `/construction/submit`.
+type TransactionIdentifierResponse struct {
 	TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"`
 	Metadata              map[string]interface{} `json:"metadata,omitempty"`
 }


Test patch:
diff --git a/asserter/account_test.go b/asserter/account_test.go
index 391bd07..c831204 100644
--- a/asserter/account_test.go
+++ b/asserter/account_test.go
@@ -250,10 +250,16 @@ func TestAccoutBalance(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			err := AccountBalanceResponse(
 				test.requestBlock,
-				test.responseBlock,
-				test.balances,
+				&types.AccountBalanceResponse{
+					BlockIdentifier: test.responseBlock,
+					Balances:        test.balances,
+				},
 			)
-			assert.Equal(t, test.err, err)
+			if test.err != nil {
+				assert.Contains(t, err.Error(), test.err.Error())
+			} else {
+				assert.NoError(t, err)
+			}
 		})
 	}
 }
diff --git a/asserter/block_test.go b/asserter/block_test.go
index e5ce6ab..28e91e2 100644
--- a/asserter/block_test.go
+++ b/asserter/block_test.go
@@ -471,7 +471,12 @@ func TestOperation(t *testing.T) {
 
 		t.Run(name, func(t *testing.T) {
 			err := asserter.Operation(test.operation, test.index, test.construction)
-			assert.Equal(t, test.err, err)
+			if test.err != nil {
+				assert.Contains(t, err.Error(), test.err.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+
 			if err == nil && !test.construction {
 				successful, err := asserter.OperationSuccessful(test.operation)
 				assert.NoError(t, err)
diff --git a/asserter/coin_test.go b/asserter/coin_test.go
new file mode 100644
index 0000000..645f1c5
--- /dev/null
+++ b/asserter/coin_test.go
@@ -0,0 +1,191 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package asserter
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/coinbase/rosetta-sdk-go/types"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCoin(t *testing.T) {
+	var tests = map[string]struct {
+		coin *types.Coin
+		err  error
+	}{
+		"valid coin": {
+			coin: &types.Coin{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "coin1",
+				},
+				Amount: validAmount,
+			},
+			err: nil,
+		},
+		"nil": {
+			coin: nil,
+			err:  errors.New("cannot be nil"),
+		},
+		"invalid identifier": {
+			coin: &types.Coin{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "",
+				},
+				Amount: validAmount,
+			},
+			err: errors.New("identifier is invalid"),
+		},
+		"invalid amount": {
+			coin: &types.Coin{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "coin1",
+				},
+				Amount: &types.Amount{
+					Value: "100",
+				},
+			},
+			err: errors.New("amount is invalid"),
+		},
+		"nil amount": {
+			coin: &types.Coin{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "coin1",
+				},
+			},
+			err: errors.New("amount is invalid"),
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			err := Coin(test.coin)
+			if test.err != nil {
+				assert.Contains(t, err.Error(), test.err.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestCoins(t *testing.T) {
+	var tests = map[string]struct {
+		coins []*types.Coin
+		err   error
+	}{
+		"valid coins": {
+			coins: []*types.Coin{
+				{
+					CoinIdentifier: &types.CoinIdentifier{
+						Identifier: "coin1",
+					},
+					Amount: validAmount,
+				},
+				{
+					CoinIdentifier: &types.CoinIdentifier{
+						Identifier: "coin2",
+					},
+					Amount: validAmount,
+				},
+			},
+			err: nil,
+		},
+		"nil": {
+			coins: nil,
+			err:   nil,
+		},
+		"duplicate coins": {
+			coins: []*types.Coin{
+				{
+					CoinIdentifier: &types.CoinIdentifier{
+						Identifier: "coin1",
+					},
+					Amount: validAmount,
+				},
+				{
+					CoinIdentifier: &types.CoinIdentifier{
+						Identifier: "coin1",
+					},
+					Amount: validAmount,
+				},
+			},
+			err: errors.New("duplicate coin identifier"),
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			err := Coins(test.coins)
+			if test.err != nil {
+				assert.Contains(t, err.Error(), test.err.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestCoinChange(t *testing.T) {
+	var tests = map[string]struct {
+		change *types.CoinChange
+		err    error
+	}{
+		"valid change": {
+			change: &types.CoinChange{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "coin1",
+				},
+				CoinAction: types.CoinCreated,
+			},
+			err: nil,
+		},
+		"nil": {
+			change: nil,
+			err:    errors.New("cannot be nil"),
+		},
+		"invalid identifier": {
+			change: &types.CoinChange{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "",
+				},
+				CoinAction: types.CoinCreated,
+			},
+			err: errors.New("cannot be empty"),
+		},
+		"invalid coin action": {
+			change: &types.CoinChange{
+				CoinIdentifier: &types.CoinIdentifier{
+					Identifier: "coin1",
+				},
+				CoinAction: "hello",
+			},
+			err: errors.New("not a valid coin action"),
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			err := CoinChange(test.change)
+			if test.err != nil {
+				assert.Contains(t, err.Error(), test.err.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/asserter/construction_test.go b/asserter/construction_test.go
index aa65940..656df08 100644
--- a/asserter/construction_test.go
+++ b/asserter/construction_test.go
@@ -51,13 +51,13 @@ func TestConstructionMetadataResponse(t *testing.T) {
 	}
 }
 
-func TestConstructionSubmitResponse(t *testing.T) {
+func TestTransactionIdentifierResponse(t *testing.T) {
 	var tests = map[string]struct {
-		response *types.ConstructionSubmitResponse
+		response *types.TransactionIdentifierResponse
 		err      error
 	}{
 		"valid response": {
-			response: &types.ConstructionSubmitResponse{
+			response: &types.TransactionIdentifierResponse{
 				TransactionIdentifier: &types.TransactionIdentifier{
 					Hash: "tx1",
 				},
@@ -68,14 +68,14 @@ func TestConstructionSubmitResponse(t *testing.T) {
 			err: errors.New("construction submit response cannot be nil"),
 		},
 		"invalid transaction identifier": {
-			response: &types.ConstructionSubmitResponse{},
+			response: &types.TransactionIdentifierResponse{},
 			err:      errors.New("TransactionIdentifier is nil"),
 		},
 	}
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			err := ConstructionSubmitResponse(test.response)
+			err := TransactionIdentifierResponse(test.response)
 			assert.Equal(t, test.err, err)
 		})
 	}
@@ -144,34 +144,6 @@ func TestConstructionDeriveResponse(t *testing.T) {
 	}
 }
 
-func TestConstructionHashResponse(t *testing.T) {
-	var tests = map[string]struct {
-		response *types.ConstructionHashResponse
-		err      error
-	}{
-		"valid response": {
-			response: &types.ConstructionHashResponse{
-				TransactionHash: "tx hash",
-			},
-			err: nil,
-		},
-		"nil response": {
-			err: errors.New("construction hash response cannot be nil"),
-		},
-		"empty tx hash": {
-			response: &types.ConstructionHashResponse{},
-			err:      errors.New("transaction hash cannot be empty"),
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			err := ConstructionHashResponse(test.response)
-			assert.Equal(t, test.err, err)
-		})
-	}
-}
-
 func TestConstructionParseResponse(t *testing.T) {
 	var tests = map[string]struct {
 		response *types.ConstructionParseResponse
@@ -238,7 +210,7 @@ func TestConstructionParseResponse(t *testing.T) {
 					"extra": "stuff",
 				},
 			},
-			err: errors.New("Operation.OperationIdentifier.Index 1 is out of order, expected 0 unable to parse operations"),
+			err: errors.New("Operation.OperationIdentifier.Index 1 is out of order, expected 0"),
 		},
 		"no signers": {
 			response: &types.ConstructionParseResponse{
diff --git a/fetcher/account_test.go b/fetcher/account_test.go
index e3b9740..8a393c4 100644
--- a/fetcher/account_test.go
+++ b/fetcher/account_test.go
@@ -149,7 +149,7 @@ func TestAccountBalanceRetry(t *testing.T) {
 				WithRetryElapsedTime(5*time.Second),
 				WithMaxRetries(test.fetcherMaxRetries),
 			)
-			block, amounts, metadata, err := f.AccountBalanceRetry(
+			block, amounts, _, metadata, err := f.AccountBalanceRetry(
 				ctx,
 				test.network,
 				test.account,
diff --git a/parser/intent_test.go b/parser/intent_test.go
index 11a3389..ab678ce 100644
--- a/parser/intent_test.go
+++ b/parser/intent_test.go
@@ -175,10 +175,9 @@ func TestExpectedOperation(t *testing.T) {
 
 func TestExpectedOperations(t *testing.T) {
 	var tests = map[string]struct {
-		intent         []*types.Operation
-		observed       []*types.Operation
-		errExtra       bool
-		confirmSuccess bool
+		intent   []*types.Operation
+		observed []*types.Operation
+		errExtra bool
 
 		err bool
 	}{
@@ -243,131 +242,6 @@ func TestExpectedOperations(t *testing.T) {
 				},
 			},
 		},
-		"simple match (confirm success)": {
-			intent: []*types.Operation{
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					Type: "transfer",
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Amount: &types.Amount{
-						Value: "100",
-					},
-				},
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 5,
-					},
-					Type: "fee",
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Amount: &types.Amount{
-						Value: "50",
-					},
-				},
-			},
-			observed: []*types.Operation{
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 2,
-					},
-					Status: "success",
-					Type:   "fee",
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Amount: &types.Amount{
-						Value: "50",
-					},
-				},
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 3,
-					},
-					RelatedOperations: []*types.OperationIdentifier{
-						{
-							Index: 2,
-						},
-					},
-					Status: "success",
-					Type:   "transfer",
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Amount: &types.Amount{
-						Value: "100",
-					},
-				},
-			},
-			confirmSuccess: true,
-		},
-		"simple match (confirm success) err": {
-			intent: []*types.Operation{
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 1,
-					},
-					Type: "transfer",
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Amount: &types.Amount{
-						Value: "100",
-					},
-				},
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 5,
-					},
-					Type: "fee",
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Amount: &types.Amount{
-						Value: "50",
-					},
-				},
-			},
-			observed: []*types.Operation{
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 2,
-					},
-					Status: "success",
-					Type:   "fee",
-					Account: &types.AccountIdentifier{
-						Address: "addr2",
-					},
-					Amount: &types.Amount{
-						Value: "50",
-					},
-				},
-				{
-					OperationIdentifier: &types.OperationIdentifier{
-						Index: 3,
-					},
-					RelatedOperations: []*types.OperationIdentifier{
-						{
-							Index: 2,
-						},
-					},
-					Status: "failure",
-					Type:   "transfer",
-					Account: &types.AccountIdentifier{
-						Address: "addr1",
-					},
-					Amount: &types.Amount{
-						Value: "100",
-					},
-				},
-			},
-			confirmSuccess: true,
-			err:            true,
-		},
 		"err extra": {
 			intent: []*types.Operation{
 				{
@@ -471,28 +345,8 @@ func TestExpectedOperations(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		asserter, err := simpleAsserterConfiguration([]*types.OperationStatus{
-			{
-				Status:     "success",
-				Successful: true,
-			},
-			{
-				Status:     "failure",
-				Successful: false,
-			},
-		})
-		assert.NoError(t, err)
-		assert.NotNil(t, asserter)
-
-		parser := New(asserter, nil)
-
 		t.Run(name, func(t *testing.T) {
-			err := parser.ExpectedOperations(
-				test.intent,
-				test.observed,
-				test.errExtra,
-				test.confirmSuccess,
-			)
+			err := ExpectedOperations(test.intent, test.observed, test.errExtra)
 			if test.err {
 				assert.Error(t, err)
 			} else {