Repository: coinbase/rosetta-sdk-go (go)

Issue:
Support parse sub account as optional
**Is your feature request related to a problem? Please describe.**
<!-- Describe what the problem is. For example, “I'm always frustrated when [...]”. -->

As of the parser of SDK, the sub account is either required or not.  There is no option to make subaccount field as optional.

**Describe the solution you'd like**
<!-- Describe what you want to happen. -->
A flag to parse account with subaccount string as optional.

**Describe alternatives you've considered**
<!-- Describe any alternative solutions or features you've considered. -->

**Additional context**
<!-- Add any other context or screenshots about the feature request in this section. -->



Pull request description:
add sub account optional flag
Fixes #403  .

### Motivation
In some rosetta implementation `sub account` field is optional. This means we can't predict if it will be there or not. The current matcher logic requires it to be either required or not required. 

### Solution
Added a flag called `SubAccountOptional` which if set to `true` will ignore the subaccount matching and if false, behaves exactly as before

### Open questions
<!--
(optional) Any open questions or feedback on design desired?
-->


Patch:
diff --git a/go.mod b/go.mod
index 2726fad..b6295df 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/neilotoole/errgroup v0.1.6
 	github.com/segmentio/fasthash v1.0.3
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.2
 	github.com/tidwall/gjson v1.14.1
 	github.com/tidwall/sjson v1.2.4
 	github.com/vmihailenco/msgpack/v5 v5.3.5
@@ -48,5 +48,5 @@ require (
 	golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
 	golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
 	google.golang.org/protobuf v1.25.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 0332826..c6c1b81 100644
--- a/go.sum
+++ b/go.sum
@@ -394,8 +394,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
 github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
@@ -679,8 +680,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/parser/match_operations.go b/parser/match_operations.go
index 0a1a720..fc2a0d8 100644
--- a/parser/match_operations.go
+++ b/parser/match_operations.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Coinbase, Inc.
+// Copyright 2022 Coinbase, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -105,7 +105,11 @@ type MetadataDescription struct {
 
 // AccountDescription is used to describe a *types.AccountIdentifier.
 type AccountDescription struct {
-	Exists                 bool
+	Exists bool
+
+	// SubAccountOptional If this is true then SubAccountExists, SubAccountAddress,
+	// SubAccountMetadataKeys matching is ignored
+	SubAccountOptional     bool
 	SubAccountExists       bool
 	SubAccountAddress      string
 	SubAccountMetadataKeys []*MetadataDescription
@@ -213,6 +217,16 @@ func accountMatch(req *AccountDescription, account *types.AccountIdentifier) err
 		return nil
 	}
 
+	if req.SubAccountOptional {
+		// Optionally can require a certain subaccount address if subaccount is present
+		if account.SubAccount != nil {
+			if err := verifySubAccountAddress(req.SubAccountAddress, account.SubAccount); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
 	if account.SubAccount == nil {
 		if req.SubAccountExists {
 			return ErrAccountMatchSubAccountMissing
@@ -226,13 +240,8 @@ func accountMatch(req *AccountDescription, account *types.AccountIdentifier) err
 	}
 
 	// Optionally can require a certain subaccount address
-	if len(req.SubAccountAddress) > 0 && account.SubAccount.Address != req.SubAccountAddress {
-		return fmt.Errorf(
-			"%w: expected %s but got %s",
-			ErrAccountMatchUnexpectedSubAccountAddr,
-			req.SubAccountAddress,
-			account.SubAccount.Address,
-		)
+	if err := verifySubAccountAddress(req.SubAccountAddress, account.SubAccount); err != nil {
+		return err
 	}
 
 	if err := metadataMatch(req.SubAccountMetadataKeys, account.SubAccount.Metadata); err != nil {
@@ -242,6 +251,23 @@ func accountMatch(req *AccountDescription, account *types.AccountIdentifier) err
 	return nil
 }
 
+// verifySubAccountAddress verifies the sub-account address if
+// sub-account is present.
+func verifySubAccountAddress(
+	subAccountAddress string,
+	subAccount *types.SubAccountIdentifier,
+) error {
+	if len(subAccountAddress) > 0 && subAccount.Address != subAccountAddress {
+		return fmt.Errorf(
+			"%w: expected %s but got %s",
+			ErrAccountMatchUnexpectedSubAccountAddr,
+			subAccountAddress,
+			subAccount.Address,
+		)
+	}
+	return nil
+}
+
 // amountMatch returns an error if a *types.Amount does not meet an
 // *AmountDescription.
 func amountMatch(req *AmountDescription, amount *types.Amount) error {


Test patch:
diff --git a/parser/match_operations_test.go b/parser/match_operations_test.go
index 9b25c31..f917f1f 100644
--- a/parser/match_operations_test.go
+++ b/parser/match_operations_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Coinbase, Inc.
+// Copyright 2022 Coinbase, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -1408,6 +1408,108 @@ func TestMatchOperations(t *testing.T) {
 			matches: nil,
 			err:     true,
 		},
+		"sub account optional - when sub-account exist": {
+			operations: []*types.Operation{
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+						SubAccount: &types.SubAccountIdentifier{
+							Address: "sub 2",
+						},
+					},
+				},
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+				},
+			},
+			descriptions: &Descriptions{
+				OperationDescriptions: []*OperationDescription{
+					{
+						Account: &AccountDescription{
+							Exists:             true,
+							SubAccountOptional: true,
+							SubAccountExists:   true,
+							SubAccountAddress:  "sub 2",
+						},
+					},
+					{
+						Account: &AccountDescription{
+							Exists:             true,
+							SubAccountOptional: true,
+							SubAccountExists:   true,
+							SubAccountAddress:  "sub 1",
+						},
+					},
+				},
+			},
+			matches: []*Match{
+				{
+					Operations: []*types.Operation{
+						{
+							Account: &types.AccountIdentifier{
+								Address: "addr1",
+								SubAccount: &types.SubAccountIdentifier{
+									Address: "sub 2",
+								},
+							},
+						},
+					},
+					Amounts: []*big.Int{nil},
+				},
+				{
+					Operations: []*types.Operation{
+						{
+							Account: &types.AccountIdentifier{
+								Address: "addr2",
+							},
+						},
+					},
+					Amounts: []*big.Int{nil},
+				},
+			},
+			err: false,
+		},
+		"sub account optional - when sub-account address is different": {
+			operations: []*types.Operation{
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+						SubAccount: &types.SubAccountIdentifier{
+							Address: "sub 2",
+						},
+					},
+				},
+				{
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+				},
+			},
+			descriptions: &Descriptions{
+				OperationDescriptions: []*OperationDescription{
+					{
+						Account: &AccountDescription{
+							Exists:             true,
+							SubAccountOptional: true,
+							SubAccountExists:   true,
+							SubAccountAddress:  "sub 3",
+						},
+					},
+					{
+						Account: &AccountDescription{
+							Exists:             true,
+							SubAccountOptional: true,
+							SubAccountExists:   true,
+							SubAccountAddress:  "sub 1",
+						},
+					},
+				},
+			},
+			matches: nil,
+			err:     true,
+		},
 		"nil descriptions": {
 			operations: []*types.Operation{
 				{