Repository: coinbase/rosetta-sdk-go
Pull request #86 (2020-07-27 19:08:26)

Parse Coins
Related PR: https://github.com/coinbase/rosetta-cli/pull/81

This _should_ be the last PR before the next release. It changes the logging verbosity for exempt operations and adds support for parsing the new `Coin` model when matching operations in the `parser`.

### Changes
- [x] don't log exempt ops
- [X] parser support for coin model


Patch:
diff --git a/parser/balance_changes.go b/parser/balance_changes.go
index ac03575..4e0e864 100644
--- a/parser/balance_changes.go
+++ b/parser/balance_changes.go
@@ -17,7 +17,6 @@ package parser
 import (
 	"context"
 	"fmt"
-	"log"
 
 	"github.com/coinbase/rosetta-sdk-go/types"
 )
@@ -61,7 +60,6 @@ func (p *Parser) skipOperation(op *types.Operation) (bool, error) {
 	// In some cases, it may be desirable to exempt certain operations from
 	// balance changes.
 	if p.ExemptFunc != nil && p.ExemptFunc(op) {
-		log.Printf("Skipping exempt operation %s\n", types.PrettyPrintStruct(op))
 		return true, nil
 	}
 
diff --git a/parser/match_operations.go b/parser/match_operations.go
index da1f6cb..74aa29a 100644
--- a/parser/match_operations.go
+++ b/parser/match_operations.go
@@ -118,6 +118,11 @@ type OperationDescription struct {
 	// Optional indicates that not finding any operations that meet
 	// the description should not trigger an error.
 	Optional bool
+
+	// CoinAction indicates that an operation should have a CoinChange
+	// and that it should have the CoinAction. If this is not populated,
+	// CoinChange is not checked.
+	CoinAction types.CoinAction
 }
 
 // Descriptions contains a slice of OperationDescriptions and
@@ -244,6 +249,26 @@ func amountMatch(req *AmountDescription, amount *types.Amount) error {
 	return nil
 }
 
+func coinActionMatch(requiredAction types.CoinAction, coinChange *types.CoinChange) error {
+	if len(requiredAction) == 0 {
+		return nil
+	}
+
+	if coinChange == nil {
+		return fmt.Errorf("coin change is nil but expected %s", requiredAction)
+	}
+
+	if coinChange.CoinAction != requiredAction {
+		return fmt.Errorf(
+			"coin change action is %s but expected %s",
+			coinChange.CoinAction,
+			requiredAction,
+		)
+	}
+
+	return nil
+}
+
 // operationMatch returns an error if a *types.Operation does not match a
 // *OperationDescription.
 func operationMatch(
@@ -272,6 +297,10 @@ func operationMatch(
 			continue
 		}
 
+		if err := coinActionMatch(des.CoinAction, operation.CoinChange); err != nil {
+			continue
+		}
+
 		if matches[i] == nil {
 			matches[i] = &Match{
 				Operations: []*types.Operation{},


Test patch:
diff --git a/parser/match_operations_test.go b/parser/match_operations_test.go
index 2f23bc6..3d0ed96 100644
--- a/parser/match_operations_test.go
+++ b/parser/match_operations_test.go
@@ -382,6 +382,172 @@ func TestMatchOperations(t *testing.T) {
 			},
 			err: false,
 		},
+		"simple transfer (with coin action)": {
+			operations: []*types.Operation{
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+						Currency: &types.Currency{
+							Symbol:   "BTC",
+							Decimals: 8,
+						},
+					},
+					CoinChange: &types.CoinChange{
+						CoinAction: types.CoinSpent,
+					},
+				},
+				{}, // extra op ignored
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "-100",
+						Currency: &types.Currency{
+							Symbol:   "ETH",
+							Decimals: 18,
+						},
+					},
+				},
+			},
+			descriptions: &Descriptions{
+				OppositeAmounts: [][]int{{0, 1}},
+				OperationDescriptions: []*OperationDescription{
+					{
+						Account: &AccountDescription{
+							Exists: true,
+						},
+						Amount: &AmountDescription{
+							Exists: true,
+							Sign:   NegativeAmountSign,
+							Currency: &types.Currency{
+								Symbol:   "ETH",
+								Decimals: 18,
+							},
+						},
+					},
+					{
+						Account: &AccountDescription{
+							Exists: true,
+						},
+						Amount: &AmountDescription{
+							Exists: true,
+							Sign:   PositiveAmountSign,
+							Currency: &types.Currency{
+								Symbol:   "BTC",
+								Decimals: 8,
+							},
+						},
+						CoinAction: types.CoinSpent,
+					},
+				},
+			},
+			matches: []*Match{
+				{
+					Operations: []*types.Operation{
+						{
+							Account: &types.AccountIdentifier{
+								Address: "addr1",
+							},
+							Amount: &types.Amount{
+								Value: "-100",
+								Currency: &types.Currency{
+									Symbol:   "ETH",
+									Decimals: 18,
+								},
+							},
+						},
+					},
+					Amounts: []*big.Int{big.NewInt(-100)},
+				},
+				{
+					Operations: []*types.Operation{
+						{
+							Account: &types.AccountIdentifier{
+								Address: "addr2",
+							},
+							Amount: &types.Amount{
+								Value: "100",
+								Currency: &types.Currency{
+									Symbol:   "BTC",
+									Decimals: 8,
+								},
+							},
+							CoinChange: &types.CoinChange{
+								CoinAction: types.CoinSpent,
+							},
+						},
+					},
+					Amounts: []*big.Int{big.NewInt(100)},
+				},
+			},
+			err: false,
+		},
+		"simple transfer (missing coin action)": {
+			operations: []*types.Operation{
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+						Currency: &types.Currency{
+							Symbol:   "BTC",
+							Decimals: 8,
+						},
+					},
+				},
+				{}, // extra op ignored
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "-100",
+						Currency: &types.Currency{
+							Symbol:   "ETH",
+							Decimals: 18,
+						},
+					},
+				},
+			},
+			descriptions: &Descriptions{
+				OppositeAmounts: [][]int{{0, 1}},
+				OperationDescriptions: []*OperationDescription{
+					{
+						Account: &AccountDescription{
+							Exists: true,
+						},
+						Amount: &AmountDescription{
+							Exists: true,
+							Sign:   NegativeAmountSign,
+							Currency: &types.Currency{
+								Symbol:   "ETH",
+								Decimals: 18,
+							},
+						},
+					},
+					{
+						Account: &AccountDescription{
+							Exists: true,
+						},
+						Amount: &AmountDescription{
+							Exists: true,
+							Sign:   PositiveAmountSign,
+							Currency: &types.Currency{
+								Symbol:   "BTC",
+								Decimals: 8,
+							},
+						},
+						CoinAction: types.CoinSpent,
+					},
+				},
+			},
+			err: true,
+		},
 		"simple transfer (with currency)": {
 			operations: []*types.Operation{
 				{