diff --git a/Makefile b/Makefile index 81fd7181..1ff6952e 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,23 @@ -.PHONY: gen-deps deps gen lint format check-format test test-coverage add-license \ +.PHONY: deps gen lint format check-format test test-coverage add-license \ check-license shorten-lines shellcheck salus release -LICENCE_SCRIPT=addlicense -c "Coinbase, Inc." -l "apache" -v -GO_PACKAGES=./asserter/... ./fetcher/... ./types/... ./client/... ./server/... + +# To run the the following packages as commands, +# it is necessary to use `go run `. Running `go get` does +# not install any binaries that could be used to run +# the commands directly. +ADDLICENSE_CMD=go run github.com/google/addlicense +ADDLICENCE_SCRIPT=${ADDLICENSE_CMD} -c "Coinbase, Inc." -l "apache" -v +GOIMPORTS_CMD=go run golang.org/x/tools/cmd/goimports +GOLINES_CMD=go run github.com/segmentio/golines +GOVERALLS_CMD=go run github.com/mattn/goveralls + +GO_PACKAGES=./asserter/... ./fetcher/... ./types/... ./client/... ./server/... ./parser/... GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g") -GO_INSTALL=GO111MODULE=off go get TEST_SCRIPT=go test -v ${GO_PACKAGES} LINT_SETTINGS=golint,misspell,gocyclo,gocritic,whitespace,goconst,gocognit,bodyclose,unconvert,lll,unparam -deps: | gen-deps +deps: go get ./... - ${GO_INSTALL} github.com/mattn/goveralls - -gen-deps: - ${GO_INSTALL} github.com/google/addlicense - ${GO_INSTALL} github.com/segmentio/golines - ${GO_INSTALL} golang.org/x/tools/cmd/goimports gen: ./codegen.sh @@ -31,27 +34,27 @@ lint: | lint-examples format: gofmt -s -w -l . - goimports -w . + ${GOIMPORTS_CMD} -w . check-format: ! gofmt -s -l . | read - ! goimports -l . | read + ! ${GOIMPORTS_CMD} -l . | read test: ${TEST_SCRIPT} test-cover: ${TEST_SCRIPT} -coverprofile=c.out -covermode=count - goveralls -coverprofile=c.out -repotoken ${COVERALLS_TOKEN} + ${GOVERALLS_CMD} -coverprofile=c.out -repotoken ${COVERALLS_TOKEN} add-license: - ${LICENCE_SCRIPT} . + ${ADDLICENCE_SCRIPT} . check-license: - ${LICENCE_SCRIPT} -check . + ${ADDLICENCE_SCRIPT} -check . shorten-lines: - golines -w --shorten-comments ${GO_FOLDERS} examples + ${GOLINES_CMD} -w --shorten-comments ${GO_FOLDERS} examples shellcheck: shellcheck codegen.sh diff --git a/README.md b/README.md index 4565ce1f..e4c1bada 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ and network-specific work. * [Asserter](asserter): Validation of Rosetta types * [Fetcher](fetcher): Simplified and validated communication with any Rosetta server +* [Parser](parser): Tool for parsing Rosetta blocks ## Examples The packages listed above are demoed extensively in diff --git a/asserter/block.go b/asserter/block.go index 0726bfa4..ae448904 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -52,8 +52,8 @@ func Amount(amount *types.Amount) error { return errors.New("Amount.Currency.Symbol is empty") } - if amount.Currency.Decimals <= 0 { - return errors.New("Amount.Currency.Decimals must be > 0") + if amount.Currency.Decimals < 0 { + return errors.New("Amount.Currency.Decimals must be >= 0") } return nil @@ -66,10 +66,18 @@ func OperationIdentifier( identifier *types.OperationIdentifier, index int64, ) error { - if identifier == nil || identifier.Index != index { + if identifier == nil { return errors.New("Operation.OperationIdentifier.Index invalid") } + if identifier.Index != index { + return fmt.Errorf( + "Operation.OperationIdentifier.Index %d is out of order, expected %d", + identifier.Index, + index, + ) + } + if identifier.NetworkIndex != nil && *identifier.NetworkIndex < 0 { return errors.New("Operation.OperationIdentifier.NetworkIndex invalid") } @@ -99,9 +107,21 @@ func AccountIdentifier(account *types.AccountIdentifier) error { return nil } -// contains checks if a string is contained in a slice +// containsString checks if an string is contained in a slice // of strings. -func contains(valid []string, value string) bool { +func containsString(valid []string, value string) bool { + for _, v := range valid { + if v == value { + return true + } + } + + return false +} + +// containsInt64 checks if an int64 is contained in a slice +// of Int64. +func containsInt64(valid []int64, value int64) bool { for _, v := range valid { if v == value { return true @@ -136,7 +156,7 @@ func (a *Asserter) OperationType(t string) error { return ErrAsserterNotInitialized } - if t == "" || !contains(a.operationTypes, t) { + if t == "" || !containsString(a.operationTypes, t) { return fmt.Errorf("Operation.Type %s is invalid", t) } @@ -255,9 +275,33 @@ func (a *Asserter) Transaction( } for i, op := range transaction.Operations { + // Ensure operations are sorted if err := a.Operation(op, int64(i)); err != nil { return err } + + // Ensure an operation's related_operations are only + // operations with an index less than the operation + // and that there are no duplicates. + relatedIndexes := []int64{} + for _, relatedOp := range op.RelatedOperations { + if relatedOp.Index >= op.OperationIdentifier.Index { + return fmt.Errorf( + "related operation index %d >= operation index %d", + relatedOp.Index, + op.OperationIdentifier.Index, + ) + } + + if containsInt64(relatedIndexes, relatedOp.Index) { + return fmt.Errorf( + "found duplicate related operation index %d for operation index %d", + relatedOp.Index, + op.OperationIdentifier.Index, + ) + } + relatedIndexes = append(relatedIndexes, relatedOp.Index) + } } return nil diff --git a/asserter/block_test.go b/asserter/block_test.go index 0cf14864..6f8f1434 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -79,6 +79,15 @@ func TestAmount(t *testing.T) { }, err: nil, }, + "valid amount no decimals": { + amount: &types.Amount{ + Value: "100000", + Currency: &types.Currency{ + Symbol: "BTC", + }, + }, + err: nil, + }, "valid negative amount": { amount: &types.Amount{ Value: "-100000", @@ -142,10 +151,11 @@ func TestAmount(t *testing.T) { amount: &types.Amount{ Value: "111", Currency: &types.Currency{ - Symbol: "BTC", + Symbol: "BTC", + Decimals: -1, }, }, - err: errors.New("Amount.Currency.Decimals must be > 0"), + err: errors.New("Amount.Currency.Decimals must be >= 0"), }, } @@ -185,7 +195,7 @@ func TestOperationIdentifier(t *testing.T) { Index: 0, }, index: 1, - err: errors.New("Operation.OperationIdentifier.Index invalid"), + err: errors.New("Operation.OperationIdentifier.Index 0 is out of order, expected 1"), }, "valid identifier with network index": { identifier: &types.OperationIdentifier{ @@ -344,7 +354,7 @@ func TestOperation(t *testing.T) { Status: "SUCCESS", }, index: int64(2), - err: errors.New("Operation.OperationIdentifier.Index invalid"), + err: errors.New("Operation.OperationIdentifier.Index 1 is out of order, expected 2"), }, "invalid operation invalid type": { operation: &types.Operation{ @@ -450,10 +460,164 @@ func TestBlock(t *testing.T) { Hash: "blah parent", Index: 99, } + validAmount := &types.Amount{ + Value: "1000", + Currency: &types.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + validAccount := &types.AccountIdentifier{ + Address: "test", + } validTransaction := &types.Transaction{ TransactionIdentifier: &types.TransactionIdentifier{ Hash: "blah", }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(0), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(1), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(0), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + }, + } + outOfOrderTransaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "blah", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(1), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(0), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(0), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + }, + } + relatedToSelfTransaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "blah", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(0), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(0), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + }, + } + relatedToLaterTransaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "blah", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(0), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(1), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(1), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(0), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + }, + } + relatedDuplicateTransaction := &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "blah", + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(0), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: int64(1), + }, + RelatedOperations: []*types.OperationIdentifier{ + { + Index: int64(0), + }, + { + Index: int64(0), + }, + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + }, } var tests = map[string]struct { block *types.Block @@ -478,6 +642,42 @@ func TestBlock(t *testing.T) { genesisIndex: validBlockIdentifier.Index, err: nil, }, + "out of order transaction operations": { + block: &types.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: MinUnixEpoch + 1, + Transactions: []*types.Transaction{outOfOrderTransaction}, + }, + err: errors.New("Operation.OperationIdentifier.Index 1 is out of order, expected 0"), + }, + "related to self transaction operations": { + block: &types.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: MinUnixEpoch + 1, + Transactions: []*types.Transaction{relatedToSelfTransaction}, + }, + err: errors.New("related operation index 0 >= operation index 0"), + }, + "related to later transaction operations": { + block: &types.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: MinUnixEpoch + 1, + Transactions: []*types.Transaction{relatedToLaterTransaction}, + }, + err: errors.New("related operation index 1 >= operation index 0"), + }, + "duplicate related transaction operations": { + block: &types.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: MinUnixEpoch + 1, + Transactions: []*types.Transaction{relatedDuplicateTransaction}, + }, + err: errors.New("found duplicate related operation index 0 for operation index 1"), + }, "nil block": { block: nil, err: errors.New("Block is nil"), diff --git a/asserter/network.go b/asserter/network.go index 3bb378a2..8dd8c8de 100644 --- a/asserter/network.go +++ b/asserter/network.go @@ -92,7 +92,7 @@ func StringArray(arrName string, arr []string) error { return fmt.Errorf("%s has an empty string", arrName) } - if contains(parsed, s) { + if containsString(parsed, s) { return fmt.Errorf("%s contains a duplicate %s", arrName, s) } diff --git a/codegen.sh b/codegen.sh index f8671972..9833dbe5 100755 --- a/codegen.sh +++ b/codegen.sh @@ -142,4 +142,4 @@ FORMAT_GEN="gofmt -w /local/types; gofmt -w /local/client; gofmt -w /local/serve GOLANG_VERSION=1.13 docker run --rm -v "${PWD}":/local \ golang:${GOLANG_VERSION} sh -c \ - "cd /local; make gen-deps; ${FORMAT_GEN}; make add-license; make shorten-lines;" + "cd /local; make deps; ${FORMAT_GEN}; make add-license; make shorten-lines;" diff --git a/go.mod b/go.mod index d667ddc8..9cd6cef8 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,12 @@ go 1.13 require ( github.com/cenkalti/backoff v2.2.1+incompatible + github.com/google/addlicense v0.0.0-20200422172452-68a83edd47bc // indirect github.com/gorilla/mux v1.7.4 + github.com/mattn/goveralls v0.0.5 // indirect github.com/mitchellh/mapstructure v1.3.0 + github.com/segmentio/golines v0.0.0-20200306054842-869934f8da7b // indirect github.com/stretchr/testify v1.5.1 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a + golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c // indirect ) diff --git a/go.sum b/go.sum index 89c8a676..b3066a65 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,107 @@ +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/dave/dst v0.23.1 h1:2obX6c3RqALrEOp6u01qsqPvwp0t+RpOp9O4Bf9KhXs= +github.com/dave/dst v0.23.1/go.mod h1:LjPcLEauK4jC5hQ1fE/wr05O41zK91Pr4Qs22Ljq7gs= +github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8= +github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/addlicense v0.0.0-20200422172452-68a83edd47bc h1:CHWlqgYPu3FMUOyAno2lVDyI9wmexZEuV6/nDvsvETc= +github.com/google/addlicense v0.0.0-20200422172452-68a83edd47bc/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA= +github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/goveralls v0.0.5 h1:spfq8AyZ0cCk57Za6/juJ5btQxeE1FaEGMdfcI+XO48= +github.com/mattn/goveralls v0.0.5/go.mod h1:Xg2LHi51faXLyKXwsndxiW6uxEEQT9+3sjGzzwU4xy0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/mapstructure v1.3.0 h1:iDwIio/3gk2QtLLEsqU5lInaMzos0hDTz8a6lazSFVw= github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/golines v0.0.0-20200306054842-869934f8da7b h1:Jk5Swz/AfwWl5yc/yquzUmNesPF2aTFuafpjikzczRg= +github.com/segmentio/golines v0.0.0-20200306054842-869934f8da7b/go.mod h1:K7zjgP8yJ/U8nb8nxaSykalAKSvbqr6TNbd9B7zzBFU= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191024172528-b4ff53e7a1cb h1:ZxSglHghKPYD8WDeRUzRJrUJtDF0PxsTUSxyqr9/5BI= +golang.org/x/sys v0.0.0-20191024172528-b4ff53e7a1cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c h1:TDspWmUQsjdWzrHnd5imfaJSfhR4AO/R7kG++T2cONw= +golang.org/x/tools v0.0.0-20200507205054-480da3ebd79c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/parser/README.md b/parser/README.md new file mode 100644 index 00000000..3b2171cd --- /dev/null +++ b/parser/README.md @@ -0,0 +1,13 @@ +# Parser + +[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=shield)](https://pkg.go.dev/github.com/coinbase/rosetta-sdk-go/parser?tab=doc) + +The Parser package provides support for parsing Rosetta blocks. This includes +things like calculating all the balance changes that occurred in a block and +grouping related operations. + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/parser +``` diff --git a/parser/balance_changes.go b/parser/balance_changes.go new file mode 100644 index 00000000..f4c81435 --- /dev/null +++ b/parser/balance_changes.go @@ -0,0 +1,137 @@ +// 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 parser + +import ( + "context" + "fmt" + "log" + + "github.com/coinbase/rosetta-sdk-go/types" +) + +// BalanceChange represents a balance change that affected +// a *types.AccountIdentifier and a *types.Currency. +type BalanceChange struct { + Account *types.AccountIdentifier `json:"account_identifier,omitempty"` + Currency *types.Currency `json:"currency,omitempty"` + Block *types.BlockIdentifier `json:"block_identifier,omitempty"` + Difference string `json:"difference,omitempty"` +} + +// exemptOperation is a function that returns a boolean indicating +// if the operation should be skipped eventhough it passes other +// checks indiciating it should be considered a balance change. +type exemptOperation func(*types.Operation) bool + +// skipOperation returns a boolean indicating whether +// an operation should be processed. An operation will +// not be processed if it is considered unsuccessful. +func (p *Parser) skipOperation(op *types.Operation) (bool, error) { + successful, err := p.Asserter.OperationSuccessful(op) + if err != nil { + // Should only occur if responses not validated + return false, err + } + + if !successful { + return true, nil + } + + if op.Account == nil { + return true, nil + } + + if op.Amount == nil { + return true, nil + } + + // 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 + } + + return false, nil +} + +// BalanceChanges returns all balance changes for +// a particular block. All balance changes for a +// particular account are summed into a single +// BalanceChanges struct. If a block is being +// orphaned, the opposite of each balance change is +// returned. +func (p *Parser) BalanceChanges( + ctx context.Context, + block *types.Block, + blockRemoved bool, +) ([]*BalanceChange, error) { + balanceChanges := map[string]*BalanceChange{} + for _, tx := range block.Transactions { + for _, op := range tx.Operations { + skip, err := p.skipOperation(op) + if err != nil { + return nil, err + } + if skip { + continue + } + + amount := op.Amount + blockIdentifier := block.BlockIdentifier + if blockRemoved { + negatedValue, err := types.NegateValue(amount.Value) + if err != nil { + return nil, err + } + amount.Value = negatedValue + blockIdentifier = block.ParentBlockIdentifier + } + + // Merge values by account and currency + key := fmt.Sprintf( + "%s/%s", + types.Hash(op.Account), + types.Hash(op.Amount.Currency), + ) + + val, ok := balanceChanges[key] + if !ok { + balanceChanges[key] = &BalanceChange{ + Account: op.Account, + Currency: op.Amount.Currency, + Difference: amount.Value, + Block: blockIdentifier, + } + continue + } + + newDifference, err := types.AddValues(val.Difference, amount.Value) + if err != nil { + return nil, err + } + val.Difference = newDifference + balanceChanges[key] = val + } + } + + allChanges := []*BalanceChange{} + for _, change := range balanceChanges { + allChanges = append(allChanges, change) + } + + return allChanges, nil +} diff --git a/parser/balance_changes_test.go b/parser/balance_changes_test.go new file mode 100644 index 00000000..2c3c8242 --- /dev/null +++ b/parser/balance_changes_test.go @@ -0,0 +1,326 @@ +// 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 parser + +import ( + "context" + "testing" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/stretchr/testify/assert" +) + +func TestBalanceChanges(t *testing.T) { + var ( + currency = &types.Currency{ + Symbol: "Blah", + Decimals: 2, + } + + recipient = &types.AccountIdentifier{ + Address: "acct1", + } + + recipientAmount = &types.Amount{ + Value: "100", + Currency: currency, + } + + emptyAccountAndAmount = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + } + + emptyAmount = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + Account: recipient, + } + + recipientOperation = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + Account: recipient, + Amount: recipientAmount, + } + + recipientFailureOperation = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: "Transfer", + Status: "Failure", + Account: recipient, + Amount: recipientAmount, + } + + recipientTransaction = &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: "tx1", + }, + Operations: []*types.Operation{ + emptyAccountAndAmount, + emptyAmount, + recipientOperation, + recipientFailureOperation, + }, + } + + defaultStatus = []*types.OperationStatus{ + { + Status: "Success", + Successful: true, + }, + { + Status: "Failure", + Successful: false, + }, + } + ) + + var tests = map[string]struct { + block *types.Block + orphan bool + changes []*BalanceChange + allowedStatus []*types.OperationStatus + exemptFunc exemptOperation + err error + }{ + "simple block": { + block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Transactions: []*types.Transaction{ + recipientTransaction, + }, + Timestamp: asserter.MinUnixEpoch + 1, + }, + orphan: false, + changes: []*BalanceChange{ + { + Account: recipient, + Currency: currency, + Block: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + Difference: "100", + }, + }, + allowedStatus: defaultStatus, + err: nil, + }, + "simple block account exempt": { + block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Transactions: []*types.Transaction{ + recipientTransaction, + }, + Timestamp: asserter.MinUnixEpoch + 1, + }, + orphan: false, + changes: []*BalanceChange{}, + allowedStatus: defaultStatus, + exemptFunc: func(op *types.Operation) bool { + return types.Hash(op.Account) == + types.Hash(recipientOperation.Account) + }, + err: nil, + }, + "single account sum block": { + block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Transactions: []*types.Transaction{ + simpleTransactionFactory("tx1", "addr1", "100", currency), + simpleTransactionFactory("tx2", "addr1", "150", currency), + simpleTransactionFactory("tx3", "addr2", "150", currency), + }, + Timestamp: asserter.MinUnixEpoch + 1, + }, + orphan: false, + changes: []*BalanceChange{ + { + Account: &types.AccountIdentifier{ + Address: "addr1", + }, + Currency: currency, + Block: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + Difference: "250", + }, + { + Account: &types.AccountIdentifier{ + Address: "addr2", + }, + Currency: currency, + Block: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + Difference: "150", + }, + }, + allowedStatus: defaultStatus, + err: nil, + }, + "single account sum orphan block": { + block: &types.Block{ + BlockIdentifier: &types.BlockIdentifier{ + Hash: "1", + Index: 1, + }, + ParentBlockIdentifier: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Transactions: []*types.Transaction{ + simpleTransactionFactory("tx1", "addr1", "100", currency), + simpleTransactionFactory("tx2", "addr1", "150", currency), + simpleTransactionFactory("tx3", "addr2", "150", currency), + }, + Timestamp: asserter.MinUnixEpoch + 1, + }, + orphan: true, + changes: []*BalanceChange{ + { + Account: &types.AccountIdentifier{ + Address: "addr1", + }, + Currency: currency, + Block: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Difference: "-250", + }, + { + Account: &types.AccountIdentifier{ + Address: "addr2", + }, + Currency: currency, + Block: &types.BlockIdentifier{ + Hash: "0", + Index: 0, + }, + Difference: "-150", + }, + }, + allowedStatus: defaultStatus, + err: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + asserter, err := simpleAsserterConfiguration(test.allowedStatus) + assert.NoError(t, err) + assert.NotNil(t, asserter) + + parser := New( + asserter, + test.exemptFunc, + ) + + changes, err := parser.BalanceChanges( + context.Background(), + test.block, + test.orphan, + ) + + assert.ElementsMatch(t, test.changes, changes) + assert.Equal(t, test.err, err) + }) + } +} + +func simpleTransactionFactory( + hash string, + address string, + value string, + currency *types.Currency, +) *types.Transaction { + return &types.Transaction{ + TransactionIdentifier: &types.TransactionIdentifier{ + Hash: hash, + }, + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + Account: &types.AccountIdentifier{ + Address: address, + }, + Amount: &types.Amount{ + Value: value, + Currency: currency, + }, + }, + }, + } +} + +func simpleAsserterConfiguration( + allowedStatus []*types.OperationStatus, +) (*asserter.Asserter, error) { + return asserter.NewClientWithOptions( + &types.NetworkIdentifier{ + Blockchain: "bitcoin", + Network: "mainnet", + }, + &types.BlockIdentifier{ + Hash: "block 0", + Index: 0, + }, + []string{"Transfer"}, + allowedStatus, + []*types.Error{}, + ) +} diff --git a/parser/group_operations.go b/parser/group_operations.go new file mode 100644 index 00000000..54b2e256 --- /dev/null +++ b/parser/group_operations.go @@ -0,0 +1,109 @@ +// 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 parser + +import ( + "github.com/coinbase/rosetta-sdk-go/types" +) + +// OperationGroup is a group of related operations +// If all operations in a group have the same operation.Type, +// the Type is also populated. +type OperationGroup struct { + Type string + Operations []*types.Operation +} + +func containsInt(valid []int, value int) bool { + for _, v := range valid { + if v == value { + return true + } + } + + return false +} + +func addOperationToGroup( + destination *OperationGroup, + destinationIndex int, + assignments *[]int, + op *types.Operation, +) { + if op.Type != destination.Type && destination.Type != "" { + destination.Type = "" + } + destination.Operations = append(destination.Operations, op) + (*assignments)[op.OperationIdentifier.Index] = destinationIndex +} + +// GroupOperations parses all of a transaction's opertations and returns a slice +// of each group of related operations. This should ONLY be called on operations +// that have already been asserted for correctness. Assertion ensures there are +// no duplicate operation indexes, operations are sorted, and that operations +// only reference operations with an index less than theirs. +func GroupOperations(transaction *types.Transaction) []*OperationGroup { + ops := transaction.Operations + opGroups := map[int]*OperationGroup{} // using a map makes group merges much easier + opAssignments := make([]int, len(ops)) + for i, op := range ops { + // Create new group + if len(op.RelatedOperations) == 0 { + key := len(opGroups) + opGroups[key] = &OperationGroup{ + Type: op.Type, + Operations: []*types.Operation{op}, + } + opAssignments[i] = key + continue + } + + // Find groups to merge + groupsToMerge := []int{} + for _, relatedOp := range op.RelatedOperations { + if !containsInt(groupsToMerge, opAssignments[relatedOp.Index]) { + groupsToMerge = append(groupsToMerge, opAssignments[relatedOp.Index]) + } + } + + mergedGroupIndex := groupsToMerge[0] + mergedGroup := opGroups[mergedGroupIndex] + + // Add op to unified group + addOperationToGroup(mergedGroup, mergedGroupIndex, &opAssignments, op) + + // Merge Groups + for _, otherGroupIndex := range groupsToMerge[1:] { + otherGroup := opGroups[otherGroupIndex] + + // Add otherGroup ops to mergedGroup + for _, otherOp := range otherGroup.Operations { + addOperationToGroup(mergedGroup, mergedGroupIndex, &opAssignments, otherOp) + } + + // Delete otherGroup + delete(opGroups, otherGroupIndex) + } + } + + return func() []*OperationGroup { + sliceGroups := []*OperationGroup{} + for _, v := range opGroups { + sliceGroups = append(sliceGroups, v) + } + + return sliceGroups + }() +} diff --git a/parser/group_operations_test.go b/parser/group_operations_test.go new file mode 100644 index 00000000..6759a358 --- /dev/null +++ b/parser/group_operations_test.go @@ -0,0 +1,213 @@ +// 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 parser + +import ( + "testing" + + "github.com/coinbase/rosetta-sdk-go/types" + + "github.com/stretchr/testify/assert" +) + +func TestGroupOperations(t *testing.T) { + var tests = map[string]struct { + transaction *types.Transaction + groups []*OperationGroup + }{ + "no ops": { + transaction: &types.Transaction{}, + groups: []*OperationGroup{}, + }, + "unrelated ops": { + transaction: &types.Transaction{ + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "op 0", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: "op 1", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 2, + }, + Type: "op 2", + }, + }, + }, + groups: []*OperationGroup{ + { + Type: "op 0", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "op 0", + }, + }, + }, + { + Type: "op 1", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: "op 1", + }, + }, + }, + { + Type: "op 2", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 2, + }, + Type: "op 2", + }, + }, + }, + }, + }, + "related ops": { + transaction: &types.Transaction{ + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "type 0", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: "type 1", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 2, + }, + Type: "type 2", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 3, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 2}, + }, + Type: "type 2", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 4, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 2}, + }, + Type: "type 4", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 5, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: "type 0", + }, + }, + }, + groups: []*OperationGroup{ + { + Type: "type 0", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: "type 0", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 5, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 0}, + }, + Type: "type 0", + }, + }, + }, + { + Type: "type 1", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 1, + }, + Type: "type 1", + }, + }, + }, + { + Type: "", + Operations: []*types.Operation{ + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 2, + }, + Type: "type 2", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 3, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 2}, + }, + Type: "type 2", + }, + { + OperationIdentifier: &types.OperationIdentifier{ + Index: 4, + }, + RelatedOperations: []*types.OperationIdentifier{ + {Index: 2}, + }, + Type: "type 4", + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.ElementsMatch(t, test.groups, GroupOperations(test.transaction)) + }) + } +} diff --git a/parser/parser.go b/parser/parser.go new file mode 100644 index 00000000..709809d9 --- /dev/null +++ b/parser/parser.go @@ -0,0 +1,33 @@ +// 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 parser + +import ( + "github.com/coinbase/rosetta-sdk-go/asserter" +) + +// Parser provides support for parsing Rosetta blocks. +type Parser struct { + Asserter *asserter.Asserter + ExemptFunc exemptOperation +} + +// New creates a new Parser. +func New(asserter *asserter.Asserter, exemptFunc exemptOperation) *Parser { + return &Parser{ + Asserter: asserter, + ExemptFunc: exemptFunc, + } +} diff --git a/types/utils.go b/types/utils.go index fe4caa2c..d6bdbf3f 100644 --- a/types/utils.go +++ b/types/utils.go @@ -122,6 +122,18 @@ func SubtractValues( return newVal.String(), nil } +// NegateValue flips the sign of a value. +func NegateValue( + val string, +) (string, error) { + existing, ok := new(big.Int).SetString(val, 10) + if !ok { + return "", fmt.Errorf("%s is not an integer", val) + } + + return new(big.Int).Neg(existing).String(), nil +} + // AccountString returns a human-readable representation of a // *types.AccountIdentifier. func AccountString(account *AccountIdentifier) string { diff --git a/types/utils_test.go b/types/utils_test.go index e4b367e5..0cf8c437 100644 --- a/types/utils_test.go +++ b/types/utils_test.go @@ -196,6 +196,43 @@ func TestSubtractValues(t *testing.T) { } } +func TestNegateValue(t *testing.T) { + var tests = map[string]struct { + val string + result string + err error + }{ + "positive number": { + val: "100", + result: "-100", + err: nil, + }, + "negative number": { + val: "-100", + result: "100", + err: nil, + }, + "decimal number": { + val: "-100.1", + result: "", + err: errors.New("-100.1 is not an integer"), + }, + "non-number": { + val: "hello", + result: "", + err: errors.New("hello is not an integer"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + result, err := NegateValue(test.val) + assert.Equal(t, test.result, result) + assert.Equal(t, test.err, err) + }) + } +} + func TestGetAccountString(t *testing.T) { var tests = map[string]struct { account *AccountIdentifier