diff --git a/.gitignore b/.gitignore index 7605460d..b31f5ba7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ c.out coverage.html + +examples/server/server +examples/client/client +examples/fetcher/fetcher diff --git a/Makefile b/Makefile index 3b5673fe..28a89711 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ .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 -TEST_SCRIPT=go test -v ./asserter/... ./fetcher/... ./gen/... +GO_PACKAGES=./asserter/... ./fetcher/... ./models/... ./client/... ./server/... +GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g") +TEST_SCRIPT=go test -v ${GO_PACKAGES} +LINT_SETTINGS=golint,misspell,gocyclo,gocritic,whitespace,goconst,gocognit,bodyclose,unconvert,lll,unparam deps: go get ./... @@ -14,9 +17,12 @@ deps: gen: ./codegen.sh -lint: - golangci-lint run -v \ - -E golint,misspell,gocyclo,gocritic,whitespace,goconst,gocognit,bodyclose,unconvert,lll,unparam,gomnd +lint-examples: + cd examples; \ + golangci-lint run -v -E ${LINT_SETTINGS} + +lint: | lint-examples + golangci-lint run -v -E ${LINT_SETTINGS},gomnd format: gofmt -s -w -l . @@ -38,7 +44,7 @@ check-license: ${LICENCE_SCRIPT} -check . shorten-lines: - golines -w --shorten-comments asserter fetcher gen + golines -w --shorten-comments ${GO_FOLDERS} examples shellcheck: shellcheck codegen.sh diff --git a/README.md b/README.md index 77b0ec8f..dcf67347 100644 --- a/README.md +++ b/README.md @@ -18,30 +18,24 @@ in this specification will enable exchanges, block explorers, and wallets to integrate with much less communication overhead and network-specific work. -## Versioning -- Rosetta version: 1.3.0 - -## Installation - -```shell -go get github.com/coinbase/rosetta-sdk-go -``` - -## Automatic Assertion -When using the helper methods to access a Rosetta Server (in `fetcher/*.go`), -responses from the server are automatically checked for adherence to -the Rosetta Interface. For example, if a `BlockIdentifer` is returned without a -`Hash`, the fetch will fail. Take a look at the tests in `asserter/*_test.go` -if you are curious about what exactly is asserted. - -_It is possible, but not recommended, to bypass this assertion using the -`unsafe` helper methods available in `fetcher/*.go`._ +## Packages +* [Models](models): Auto-generated Rosetta models +* [Client](client): Low-level communication with any Rosetta server +* [Server](server): Simplified Rosetta server development +* [Asserter](asserter): Validation of Rosetta models +* [Fetcher](fetcher): Simplified and validated communication with +any Rosetta server + +## Examples +The packages listed above are demoed extensively in +[examples](examples) and are utilized throughout the +[Rosetta Validator](https://github.com/coinbase/rosetta-validator). ## Development * `make deps` to install dependencies * `make gen` to generate models and helpers * `make test` to run tests -* `make lint` to lint the source code (included generated code) +* `make lint` to lint the source code (including generated code) * `make release` to check if code passes all tests run by CircleCI ## License diff --git a/asserter/README.md b/asserter/README.md new file mode 100644 index 00000000..4b6432d7 --- /dev/null +++ b/asserter/README.md @@ -0,0 +1,17 @@ +# Asserter + +[![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/asserter?tab=overview) + +The Asserter package is used to validate the correctness of Rosetta models. It is +important to note that this validation only ensures that required fields are +populated, fields are in the correct format, and transaction operations only +contain types and statuses specified in the /network/status endpoint. + +If you want more intensive validation, try running the +[Rosetta Validator](https://github.com/coinbase/rosetta-validator). + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/asserter +``` diff --git a/asserter/account.go b/asserter/account.go index 3f5bc375..6c747541 100644 --- a/asserter/account.go +++ b/asserter/account.go @@ -18,17 +18,17 @@ import ( "fmt" "reflect" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // containsAccountIdentifier returns a boolean indicating if a -// *rosetta.AccountIdentifier is contained within a slice of -// *rosetta.AccountIdentifier. The check for equality takes -// into account everything within the rosetta.AccountIdentifier +// *models.AccountIdentifier is contained within a slice of +// *models.AccountIdentifier. The check for equality takes +// into account everything within the models.AccountIdentifier // struct (including the SubAccountIdentifier). func containsAccountIdentifier( - identifiers []*rosetta.AccountIdentifier, - identifier *rosetta.AccountIdentifier, + identifiers []*models.AccountIdentifier, + identifier *models.AccountIdentifier, ) bool { for _, ident := range identifiers { if reflect.DeepEqual(ident, identifier) { @@ -40,11 +40,11 @@ func containsAccountIdentifier( } // containsCurrency returns a boolean indicating if a -// *rosetta.Currency is contained within a slice of -// *rosetta.Currency. The check for equality takes -// into account everything within the rosetta.Currency +// *models.Currency is contained within a slice of +// *models.Currency. The check for equality takes +// into account everything within the models.Currency // struct (including currency.Metadata). -func containsCurrency(currencies []*rosetta.Currency, currency *rosetta.Currency) bool { +func containsCurrency(currencies []*models.Currency, currency *models.Currency) bool { for _, curr := range currencies { if reflect.DeepEqual(curr, currency) { return true @@ -55,12 +55,12 @@ func containsCurrency(currencies []*rosetta.Currency, currency *rosetta.Currency } // assertBalanceAmounts returns an error if a slice -// of rosetta.Amount returned as an rosetta.AccountIdentifier's +// of models.Amount returned as an models.AccountIdentifier's // balance is invalid. It is considered invalid if the same // currency is returned multiple times (these shoould be -// consolidated) or if a rosetta.Amount is considered invalid. -func assertBalanceAmounts(amounts []*rosetta.Amount) error { - currencies := make([]*rosetta.Currency, 0) +// consolidated) or if a models.Amount is considered invalid. +func assertBalanceAmounts(amounts []*models.Amount) error { + currencies := make([]*models.Currency, 0) for _, amount := range amounts { // Ensure a currency is used at most once in balance.Amounts if containsCurrency(currencies, amount.Currency) { @@ -77,19 +77,19 @@ func assertBalanceAmounts(amounts []*rosetta.Amount) error { } // AccountBalance returns an error if the provided -// rosetta.BlockIdentifier is invalid, if the same -// rosetta.AccountIdentifier appears in multiple -// rosetta.Balance structs (should be consolidated), -// or if a rosetta.Balance is considered invalid. +// models.BlockIdentifier is invalid, if the same +// models.AccountIdentifier appears in multiple +// models.Balance structs (should be consolidated), +// or if a models.Balance is considered invalid. func AccountBalance( - block *rosetta.BlockIdentifier, - balances []*rosetta.Balance, + block *models.BlockIdentifier, + balances []*models.Balance, ) error { if err := BlockIdentifier(block); err != nil { return err } - accounts := make([]*rosetta.AccountIdentifier, 0) + accounts := make([]*models.AccountIdentifier, 0) for _, balance := range balances { if err := AccountIdentifier(balance.AccountIdentifier); err != nil { return err diff --git a/asserter/account_test.go b/asserter/account_test.go index da36e399..7e72cc33 100644 --- a/asserter/account_test.go +++ b/asserter/account_test.go @@ -19,32 +19,32 @@ import ( "fmt" "testing" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" "github.com/stretchr/testify/assert" ) func TestContainsCurrency(t *testing.T) { var tests = map[string]struct { - currencies []*rosetta.Currency - currency *rosetta.Currency + currencies []*models.Currency + currency *models.Currency contains bool }{ "simple contains": { - currencies: []*rosetta.Currency{ + currencies: []*models.Currency{ { Symbol: "BTC", Decimals: 8, }, }, - currency: &rosetta.Currency{ + currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, contains: true, }, "complex contains": { - currencies: []*rosetta.Currency{ + currencies: []*models.Currency{ { Symbol: "BTC", Decimals: 8, @@ -53,7 +53,7 @@ func TestContainsCurrency(t *testing.T) { }, }, }, - currency: &rosetta.Currency{ + currency: &models.Currency{ Symbol: "BTC", Decimals: 8, Metadata: &map[string]interface{}{ @@ -63,41 +63,41 @@ func TestContainsCurrency(t *testing.T) { contains: true, }, "empty": { - currencies: []*rosetta.Currency{}, - currency: &rosetta.Currency{ + currencies: []*models.Currency{}, + currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, contains: false, }, "symbol mismatch": { - currencies: []*rosetta.Currency{ + currencies: []*models.Currency{ { Symbol: "ERX", Decimals: 8, }, }, - currency: &rosetta.Currency{ + currency: &models.Currency{ Symbol: "BTC", Decimals: 6, }, contains: false, }, "decimal mismatch": { - currencies: []*rosetta.Currency{ + currencies: []*models.Currency{ { Symbol: "BTC", Decimals: 8, }, }, - currency: &rosetta.Currency{ + currency: &models.Currency{ Symbol: "BTC", Decimals: 6, }, contains: false, }, "metadata mismatch": { - currencies: []*rosetta.Currency{ + currencies: []*models.Currency{ { Symbol: "BTC", Decimals: 8, @@ -106,7 +106,7 @@ func TestContainsCurrency(t *testing.T) { }, }, }, - currency: &rosetta.Currency{ + currency: &models.Currency{ Symbol: "BTC", Decimals: 8, Metadata: &map[string]interface{}{ @@ -127,26 +127,26 @@ func TestContainsCurrency(t *testing.T) { func TestContainsAccountIdentifier(t *testing.T) { var tests = map[string]struct { - identifiers []*rosetta.AccountIdentifier - identifier *rosetta.AccountIdentifier + identifiers []*models.AccountIdentifier + identifier *models.AccountIdentifier contains bool }{ "simple contains": { - identifiers: []*rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{ { Address: "acct1", }, }, - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", }, contains: true, }, "complex contains": { - identifiers: []*rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{ { Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct1", Metadata: &map[string]interface{}{ "blah": "hello", @@ -154,9 +154,9 @@ func TestContainsAccountIdentifier(t *testing.T) { }, }, }, - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct1", Metadata: &map[string]interface{}{ "blah": "hello", @@ -166,28 +166,28 @@ func TestContainsAccountIdentifier(t *testing.T) { contains: true, }, "simple mismatch": { - identifiers: []*rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{ { Address: "acct1", }, }, - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct2", }, contains: false, }, "empty": { - identifiers: []*rosetta.AccountIdentifier{}, - identifier: &rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{}, + identifier: &models.AccountIdentifier{ Address: "acct2", }, contains: false, }, "subaccount mismatch": { - identifiers: []*rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{ { Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct2", Metadata: &map[string]interface{}{ "blah": "hello", @@ -195,9 +195,9 @@ func TestContainsAccountIdentifier(t *testing.T) { }, }, }, - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct1", Metadata: &map[string]interface{}{ "blah": "hello", @@ -207,10 +207,10 @@ func TestContainsAccountIdentifier(t *testing.T) { contains: false, }, "metadata mismatch": { - identifiers: []*rosetta.AccountIdentifier{ + identifiers: []*models.AccountIdentifier{ { Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct1", Metadata: &map[string]interface{}{ "blah": "hello", @@ -218,9 +218,9 @@ func TestContainsAccountIdentifier(t *testing.T) { }, }, }, - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "subacct1", Metadata: &map[string]interface{}{ "blah": "bye", @@ -240,43 +240,43 @@ func TestContainsAccountIdentifier(t *testing.T) { } func TestAccoutBalance(t *testing.T) { - validBlock := &rosetta.BlockIdentifier{ + validBlock := &models.BlockIdentifier{ Index: 1000, Hash: "jsakdl", } - invalidBlock := &rosetta.BlockIdentifier{ + invalidBlock := &models.BlockIdentifier{ Index: 1, Hash: "", } - validIdentifier := &rosetta.AccountIdentifier{ + validIdentifier := &models.AccountIdentifier{ Address: "acct1", } - invalidIdentifier := &rosetta.AccountIdentifier{ + invalidIdentifier := &models.AccountIdentifier{ Address: "", } - validAmount := &rosetta.Amount{ + validAmount := &models.Amount{ Value: "100", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, } var tests = map[string]struct { - block *rosetta.BlockIdentifier - balances []*rosetta.Balance + block *models.BlockIdentifier + balances []*models.Balance err error }{ "simple balance": { block: validBlock, - balances: []*rosetta.Balance{ + balances: []*models.Balance{ { AccountIdentifier: validIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, }, }, @@ -285,10 +285,10 @@ func TestAccoutBalance(t *testing.T) { }, "invalid block": { block: invalidBlock, - balances: []*rosetta.Balance{ + balances: []*models.Balance{ { AccountIdentifier: validIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, }, }, @@ -297,10 +297,10 @@ func TestAccoutBalance(t *testing.T) { }, "invalid account identifier": { block: validBlock, - balances: []*rosetta.Balance{ + balances: []*models.Balance{ { AccountIdentifier: invalidIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, }, }, @@ -309,10 +309,10 @@ func TestAccoutBalance(t *testing.T) { }, "duplicate currency": { block: validBlock, - balances: []*rosetta.Balance{ + balances: []*models.Balance{ { AccountIdentifier: validIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, validAmount, }, @@ -322,16 +322,16 @@ func TestAccoutBalance(t *testing.T) { }, "duplicate identifier": { block: validBlock, - balances: []*rosetta.Balance{ + balances: []*models.Balance{ { AccountIdentifier: validIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, }, }, { AccountIdentifier: validIdentifier, - Amounts: []*rosetta.Amount{ + Amounts: []*models.Amount{ validAmount, }, }, diff --git a/asserter/asserter.go b/asserter/asserter.go index 7478610c..0be6c9ca 100644 --- a/asserter/asserter.go +++ b/asserter/asserter.go @@ -18,7 +18,7 @@ import ( "context" "errors" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // Asserter contains all logic to perform static @@ -26,14 +26,14 @@ import ( type Asserter struct { operationTypes []string operationStatusMap map[string]bool - errorTypeMap map[int32]*rosetta.Error + errorTypeMap map[int32]*models.Error genesisIndex int64 } // New constructs a new Asserter. func New( ctx context.Context, - networkResponse *rosetta.NetworkStatusResponse, + networkResponse *models.NetworkStatusResponse, ) (*Asserter, error) { if len(networkResponse.NetworkStatus) == 0 { return nil, errors.New("no available networks in network response") @@ -51,13 +51,13 @@ func New( } // NewOptions constructs a new Asserter using the provided -// arguments instead of using a rosetta.NetworkStatusResponse. +// arguments instead of using a models.NetworkStatusResponse. func NewOptions( ctx context.Context, - genesisBlockIdentifier *rosetta.BlockIdentifier, + genesisBlockIdentifier *models.BlockIdentifier, operationTypes []string, - operationStatuses []*rosetta.OperationStatus, - errors []*rosetta.Error, + operationStatuses []*models.OperationStatus, + errors []*models.Error, ) *Asserter { asserter := &Asserter{ operationTypes: operationTypes, @@ -69,7 +69,7 @@ func NewOptions( asserter.operationStatusMap[status.Status] = status.Successful } - asserter.errorTypeMap = map[int32]*rosetta.Error{} + asserter.errorTypeMap = map[int32]*models.Error{} for _, err := range errors { asserter.errorTypeMap[err.Code] = err } diff --git a/asserter/block.go b/asserter/block.go index 1c639404..80a308c6 100644 --- a/asserter/block.go +++ b/asserter/block.go @@ -15,17 +15,16 @@ package asserter import ( - "context" "errors" "fmt" "math/big" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) -// Amount ensures a rosetta.Amount has an +// Amount ensures a models.Amount has an // integer value, specified precision, and symbol. -func Amount(amount *rosetta.Amount) error { +func Amount(amount *models.Amount) error { if amount == nil || amount.Value == "" { return errors.New("Amount.Value is missing") } @@ -63,10 +62,10 @@ func contains(valid []string, value string) bool { } // OperationIdentifier returns an error if index of the -// rosetta.Operation is out-of-order or if the NetworkIndex is +// models.Operation is out-of-order or if the NetworkIndex is // invalid. func OperationIdentifier( - identifier *rosetta.OperationIdentifier, + identifier *models.OperationIdentifier, index int64, ) error { if identifier == nil || identifier.Index != index { @@ -80,9 +79,9 @@ func OperationIdentifier( return nil } -// AccountIdentifier returns an error if a rosetta.AccountIdentifier +// AccountIdentifier returns an error if a models.AccountIdentifier // is missing an address or a provided SubAccount is missing an identifier. -func AccountIdentifier(account *rosetta.AccountIdentifier) error { +func AccountIdentifier(account *models.AccountIdentifier) error { if account == nil { return errors.New("Account is nil") } @@ -102,10 +101,10 @@ func AccountIdentifier(account *rosetta.AccountIdentifier) error { return nil } -// OperationSuccessful returns a boolean indicating if a rosetta.Operation is +// OperationSuccessful returns a boolean indicating if a models.Operation is // successful and should be applied in a transaction. This should only be called // AFTER an operation has been validated. -func (a *Asserter) OperationSuccessful(operation *rosetta.Operation) (bool, error) { +func (a *Asserter) OperationSuccessful(operation *models.Operation) (bool, error) { val, ok := a.operationStatusMap[operation.Status] if !ok { return false, fmt.Errorf("%s not found", operation.Status) @@ -114,10 +113,10 @@ func (a *Asserter) OperationSuccessful(operation *rosetta.Operation) (bool, erro return val, nil } -// Operation ensures a rosetta.Operation has a valid +// Operation ensures a models.Operation has a valid // type, status, and amount. func (a *Asserter) Operation( - operation *rosetta.Operation, + operation *models.Operation, index int64, ) error { if operation == nil { @@ -147,10 +146,14 @@ func (a *Asserter) Operation( return Amount(operation.Amount) } -// BlockIdentifier ensures a rosetta.BlockIdentifier +// BlockIdentifier ensures a models.BlockIdentifier // is well-formatted. -func BlockIdentifier(blockIdentifier *rosetta.BlockIdentifier) error { - if blockIdentifier == nil || blockIdentifier.Hash == "" { +func BlockIdentifier(blockIdentifier *models.BlockIdentifier) error { + if blockIdentifier == nil { + return errors.New("BlockIdentifier is nil") + } + + if blockIdentifier.Hash == "" { return errors.New("BlockIdentifier.Hash is missing") } @@ -161,26 +164,48 @@ func BlockIdentifier(blockIdentifier *rosetta.BlockIdentifier) error { return nil } +// PartialBlockIdentifier ensures a models.PartialBlockIdentifier +// is well-formatted. +func PartialBlockIdentifier(blockIdentifier *models.PartialBlockIdentifier) error { + if blockIdentifier == nil { + return errors.New("PartialBlockIdentifier is nil") + } + + if blockIdentifier.Hash != nil && *blockIdentifier.Hash == "" { + return nil + } + + if blockIdentifier.Index != nil && *blockIdentifier.Index > 0 { + return nil + } + + return errors.New("neither PartialBlockIdentifier.Hash nor PartialBlockIdentifier.Index is set") +} + // TransactionIdentifier returns an error if a -// rosetta.TransactionIdentifier has an invalid hash. +// models.TransactionIdentifier has an invalid hash. func TransactionIdentifier( - transactionIdentifier *rosetta.TransactionIdentifier, + transactionIdentifier *models.TransactionIdentifier, ) error { - if transactionIdentifier == nil || transactionIdentifier.Hash == "" { - return errors.New("Transaction.TransactionIdentifier.Hash is missing") + if transactionIdentifier == nil { + return errors.New("TransactionIdentifier is nil") + } + + if transactionIdentifier.Hash == "" { + return errors.New("TransactionIdentifier.Hash is missing") } return nil } -// Transaction returns an error if the rosetta.TransactionIdentifier -// is invalid, if any rosetta.Operation within the rosetta.Transaction +// Transaction returns an error if the models.TransactionIdentifier +// is invalid, if any models.Operation within the models.Transaction // is invalid, or if any operation index is reused within a transaction. func (a *Asserter) Transaction( - transaction *rosetta.Transaction, + transaction *models.Transaction, ) error { if transaction == nil { - return errors.New("transaction is nil") + return errors.New("Transaction is nil") } if err := TransactionIdentifier(transaction.TransactionIdentifier); err != nil { @@ -208,11 +233,10 @@ func Timestamp(timestamp int64) error { // Block runs a basic set of assertions for each returned block. func (a *Asserter) Block( - ctx context.Context, - block *rosetta.Block, + block *models.Block, ) error { if block == nil { - return errors.New("block is nil") + return errors.New("Block is nil") } if err := BlockIdentifier(block.BlockIdentifier); err != nil { @@ -227,11 +251,11 @@ func (a *Asserter) Block( // genesis index. if a.genesisIndex != block.BlockIdentifier.Index { if block.BlockIdentifier.Hash == block.ParentBlockIdentifier.Hash { - return errors.New("Block.BlockIdentifier.Hash == Block.ParentBlockIdentifier.Hash") + return errors.New("BlockIdentifier.Hash == ParentBlockIdentifier.Hash") } if block.BlockIdentifier.Index <= block.ParentBlockIdentifier.Index { - return errors.New("Block.BlockIdentifier.Index <= Block.ParentBlockIdentifier.Index") + return errors.New("BlockIdentifier.Index <= ParentBlockIdentifier.Index") } } diff --git a/asserter/block_test.go b/asserter/block_test.go index 97017a17..06db3e92 100644 --- a/asserter/block_test.go +++ b/asserter/block_test.go @@ -19,20 +19,60 @@ import ( "errors" "testing" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" "github.com/stretchr/testify/assert" ) +func TestBlockIdentifier(t *testing.T) { + var tests = map[string]struct { + identifier *models.BlockIdentifier + err error + }{ + "valid identifier": { + identifier: &models.BlockIdentifier{ + Index: int64(1), + Hash: "block 1", + }, + err: nil, + }, + "nil identifier": { + identifier: nil, + err: errors.New("BlockIdentifier is nil"), + }, + "invalid index": { + identifier: &models.BlockIdentifier{ + Index: int64(-1), + Hash: "block 1", + }, + err: errors.New("BlockIdentifier.Index is negative"), + }, + "invalid hash": { + identifier: &models.BlockIdentifier{ + Index: int64(1), + Hash: "", + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := BlockIdentifier(test.identifier) + assert.Equal(t, test.err, err) + }) + } +} + func TestAmount(t *testing.T) { var tests = map[string]struct { - amount *rosetta.Amount + amount *models.Amount err error }{ "valid amount": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "100000", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 1, }, @@ -40,9 +80,9 @@ func TestAmount(t *testing.T) { err: nil, }, "valid negative amount": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "-100000", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 1, }, @@ -54,15 +94,15 @@ func TestAmount(t *testing.T) { err: errors.New("Amount.Value is missing"), }, "nil currency": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "100000", }, err: errors.New("Amount.Currency is nil"), }, "invalid non-number": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "blah", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 1, }, @@ -70,9 +110,9 @@ func TestAmount(t *testing.T) { err: errors.New("Amount.Value not an integer blah"), }, "invalid integer format": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "1.0", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 1, }, @@ -80,9 +120,9 @@ func TestAmount(t *testing.T) { err: errors.New("Amount.Value not an integer 1.0"), }, "invalid non-integer": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "1.1", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 1, }, @@ -90,18 +130,18 @@ func TestAmount(t *testing.T) { err: errors.New("Amount.Value not an integer 1.1"), }, "invalid symbol": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "11", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Decimals: 1, }, }, err: errors.New("Amount.Currency.Symbol is empty"), }, "invalid decimals": { - amount: &rosetta.Amount{ + amount: &models.Amount{ Value: "111", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", }, }, @@ -124,12 +164,12 @@ func TestOperationIdentifier(t *testing.T) { ) var tests = map[string]struct { - identifier *rosetta.OperationIdentifier + identifier *models.OperationIdentifier index int64 err error }{ "valid identifier": { - identifier: &rosetta.OperationIdentifier{ + identifier: &models.OperationIdentifier{ Index: 0, }, index: 0, @@ -141,14 +181,14 @@ func TestOperationIdentifier(t *testing.T) { err: errors.New("Operation.OperationIdentifier.Index invalid"), }, "out-of-order index": { - identifier: &rosetta.OperationIdentifier{ + identifier: &models.OperationIdentifier{ Index: 0, }, index: 1, err: errors.New("Operation.OperationIdentifier.Index invalid"), }, "valid identifier with network index": { - identifier: &rosetta.OperationIdentifier{ + identifier: &models.OperationIdentifier{ Index: 0, NetworkIndex: &validNetworkIndex, }, @@ -156,7 +196,7 @@ func TestOperationIdentifier(t *testing.T) { err: nil, }, "invalid identifier with network index": { - identifier: &rosetta.OperationIdentifier{ + identifier: &models.OperationIdentifier{ Index: 0, NetworkIndex: &invalidNetworkIndex, }, @@ -175,34 +215,34 @@ func TestOperationIdentifier(t *testing.T) { func TestAccountIdentifier(t *testing.T) { var tests = map[string]struct { - identifier *rosetta.AccountIdentifier + identifier *models.AccountIdentifier err error }{ "valid identifier": { - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", }, err: nil, }, "invalid address": { - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "", }, err: errors.New("Account.Address is missing"), }, "valid identifier with subaccount": { - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "acct2", }, }, err: nil, }, "invalid identifier with subaccount": { - identifier: &rosetta.AccountIdentifier{ + identifier: &models.AccountIdentifier{ Address: "acct1", - SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: &models.SubAccountIdentifier{ Address: "", }, }, @@ -220,28 +260,28 @@ func TestAccountIdentifier(t *testing.T) { func TestOperation(t *testing.T) { var ( - validAmount = &rosetta.Amount{ + validAmount = &models.Amount{ Value: "1000", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, } - validAccount = &rosetta.AccountIdentifier{ + validAccount = &models.AccountIdentifier{ Address: "test", } ) var tests = map[string]struct { - operation *rosetta.Operation + operation *models.Operation index int64 successful bool err error }{ "valid operation": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -254,8 +294,8 @@ func TestOperation(t *testing.T) { err: nil, }, "valid operation no account": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -271,8 +311,8 @@ func TestOperation(t *testing.T) { err: errors.New("Operation is nil"), }, "invalid operation no account": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -283,21 +323,21 @@ func TestOperation(t *testing.T) { err: errors.New("Account is nil"), }, "invalid operation empty account": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", Status: "SUCCESS", - Account: &rosetta.AccountIdentifier{}, + Account: &models.AccountIdentifier{}, Amount: validAmount, }, index: int64(1), err: errors.New("Account.Address is missing"), }, "invalid operation invalid index": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -307,8 +347,8 @@ func TestOperation(t *testing.T) { err: errors.New("Operation.OperationIdentifier.Index invalid"), }, "invalid operation invalid type": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "STAKE", @@ -318,8 +358,8 @@ func TestOperation(t *testing.T) { err: errors.New("Operation.Type STAKE is invalid"), }, "unsuccessful operation": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -330,8 +370,8 @@ func TestOperation(t *testing.T) { err: nil, }, "invalid operation invalid status": { - operation: &rosetta.Operation{ - OperationIdentifier: &rosetta.OperationIdentifier{ + operation: &models.Operation{ + OperationIdentifier: &models.OperationIdentifier{ Index: int64(1), }, Type: "PAYMENT", @@ -345,18 +385,18 @@ func TestOperation(t *testing.T) { for name, test := range tests { asserter, err := New( context.Background(), - &rosetta.NetworkStatusResponse{ - NetworkStatus: []*rosetta.NetworkStatus{ + &models.NetworkStatusResponse{ + NetworkStatus: []*models.NetworkStatus{ { - NetworkInformation: &rosetta.NetworkInformation{ - GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + NetworkInformation: &models.NetworkInformation{ + GenesisBlockIdentifier: &models.BlockIdentifier{ Index: 0, }, }, }, }, - Options: &rosetta.Options{ - OperationStatuses: []*rosetta.OperationStatus{ + Options: &models.Options{ + OperationStatuses: []*models.OperationStatus{ { Status: "SUCCESS", Successful: true, @@ -386,116 +426,116 @@ func TestOperation(t *testing.T) { } func TestBlock(t *testing.T) { - validBlockIdentifier := &rosetta.BlockIdentifier{ + validBlockIdentifier := &models.BlockIdentifier{ Hash: "blah", Index: 100, } - validParentBlockIdentifier := &rosetta.BlockIdentifier{ + validParentBlockIdentifier := &models.BlockIdentifier{ Hash: "blah parent", Index: 99, } - validTransaction := &rosetta.Transaction{ - TransactionIdentifier: &rosetta.TransactionIdentifier{ + validTransaction := &models.Transaction{ + TransactionIdentifier: &models.TransactionIdentifier{ Hash: "blah", }, } var tests = map[string]struct { - block *rosetta.Block + block *models.Block genesisIndex int64 err error }{ "valid block": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, err: nil, }, "genesis block": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validBlockIdentifier, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, genesisIndex: validBlockIdentifier.Index, err: nil, }, "nil block": { block: nil, - err: errors.New("block is nil"), + err: errors.New("Block is nil"), }, "nil block hash": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: nil, ParentBlockIdentifier: validParentBlockIdentifier, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, - err: errors.New("BlockIdentifier.Hash is missing"), + err: errors.New("BlockIdentifier is nil"), }, "invalid block hash": { - block: &rosetta.Block{ - BlockIdentifier: &rosetta.BlockIdentifier{}, + block: &models.Block{ + BlockIdentifier: &models.BlockIdentifier{}, ParentBlockIdentifier: validParentBlockIdentifier, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Hash is missing"), }, "block previous hash missing": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, - ParentBlockIdentifier: &rosetta.BlockIdentifier{}, + ParentBlockIdentifier: &models.BlockIdentifier{}, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, err: errors.New("BlockIdentifier.Hash is missing"), }, "invalid parent block index": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, - ParentBlockIdentifier: &rosetta.BlockIdentifier{ + ParentBlockIdentifier: &models.BlockIdentifier{ Hash: validParentBlockIdentifier.Hash, Index: validBlockIdentifier.Index, }, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, - err: errors.New("Block.BlockIdentifier.Index <= Block.ParentBlockIdentifier.Index"), + err: errors.New("BlockIdentifier.Index <= ParentBlockIdentifier.Index"), }, "invalid parent block hash": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, - ParentBlockIdentifier: &rosetta.BlockIdentifier{ + ParentBlockIdentifier: &models.BlockIdentifier{ Hash: validBlockIdentifier.Hash, Index: validParentBlockIdentifier.Index, }, Timestamp: 1, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, - err: errors.New("Block.BlockIdentifier.Hash == Block.ParentBlockIdentifier.Hash"), + err: errors.New("BlockIdentifier.Hash == ParentBlockIdentifier.Hash"), }, "invalid block timestamp": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, - Transactions: []*rosetta.Transaction{validTransaction}, + Transactions: []*models.Transaction{validTransaction}, }, err: errors.New("Timestamp is invalid 0"), }, "invalid block transaction": { - block: &rosetta.Block{ + block: &models.Block{ BlockIdentifier: validBlockIdentifier, ParentBlockIdentifier: validParentBlockIdentifier, Timestamp: 1, - Transactions: []*rosetta.Transaction{ + Transactions: []*models.Transaction{ {}, }, }, - err: errors.New("Transaction.TransactionIdentifier.Hash is missing"), + err: errors.New("TransactionIdentifier is nil"), }, } @@ -503,25 +543,25 @@ func TestBlock(t *testing.T) { t.Run(name, func(t *testing.T) { asserter, err := New( context.Background(), - &rosetta.NetworkStatusResponse{ - NetworkStatus: []*rosetta.NetworkStatus{ + &models.NetworkStatusResponse{ + NetworkStatus: []*models.NetworkStatus{ { - NetworkInformation: &rosetta.NetworkInformation{ - GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + NetworkInformation: &models.NetworkInformation{ + GenesisBlockIdentifier: &models.BlockIdentifier{ Index: test.genesisIndex, }, }, }, }, - Options: &rosetta.Options{ - OperationStatuses: []*rosetta.OperationStatus{}, + Options: &models.Options{ + OperationStatuses: []*models.OperationStatus{}, OperationTypes: []string{}, }, }, ) assert.NoError(t, err) - err = asserter.Block(context.Background(), test.block) + err = asserter.Block(test.block) assert.Equal(t, test.err, err) }) } diff --git a/asserter/construction.go b/asserter/construction.go index 3d41a177..0be32627 100644 --- a/asserter/construction.go +++ b/asserter/construction.go @@ -15,23 +15,23 @@ package asserter import ( - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // TransactionConstruction returns an error if -// the NetworkFee is not a valid rosetta.Amount. +// the NetworkFee is not a valid models.Amount. func TransactionConstruction( - response *rosetta.TransactionConstructionResponse, + response *models.TransactionConstructionResponse, ) error { return Amount(response.NetworkFee) } // TransactionSubmit returns an error if -// the rosetta.TransactionIdentifier in the response is not +// the models.TransactionIdentifier in the response is not // valid or if the Submission.Status is not contained // within the provided validStatuses slice. func (a *Asserter) TransactionSubmit( - response *rosetta.TransactionSubmitResponse, + response *models.TransactionSubmitResponse, ) error { if err := TransactionIdentifier(response.TransactionIdentifier); err != nil { return err diff --git a/asserter/construction_test.go b/asserter/construction_test.go index e078c7fd..1f3b100f 100644 --- a/asserter/construction_test.go +++ b/asserter/construction_test.go @@ -19,40 +19,40 @@ import ( "errors" "testing" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" "github.com/stretchr/testify/assert" ) func TestTransactionConstruction(t *testing.T) { - validAmount := &rosetta.Amount{ + validAmount := &models.Amount{ Value: "1", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, } - invalidAmount := &rosetta.Amount{ + invalidAmount := &models.Amount{ Value: "", - Currency: &rosetta.Currency{ + Currency: &models.Currency{ Symbol: "BTC", Decimals: 8, }, } var tests = map[string]struct { - response *rosetta.TransactionConstructionResponse + response *models.TransactionConstructionResponse err error }{ "valid response": { - response: &rosetta.TransactionConstructionResponse{ + response: &models.TransactionConstructionResponse{ NetworkFee: validAmount, }, err: nil, }, "valid response with metadata": { - response: &rosetta.TransactionConstructionResponse{ + response: &models.TransactionConstructionResponse{ NetworkFee: validAmount, Metadata: &map[string]interface{}{ "blah": "hello", @@ -61,7 +61,7 @@ func TestTransactionConstruction(t *testing.T) { err: nil, }, "invalid amount": { - response: &rosetta.TransactionConstructionResponse{ + response: &models.TransactionConstructionResponse{ NetworkFee: invalidAmount, }, err: errors.New("Amount.Value is missing"), @@ -78,38 +78,38 @@ func TestTransactionConstruction(t *testing.T) { func TestTransactionSubmit(t *testing.T) { var tests = map[string]struct { - response *rosetta.TransactionSubmitResponse + response *models.TransactionSubmitResponse err error }{ "valid response": { - response: &rosetta.TransactionSubmitResponse{ - TransactionIdentifier: &rosetta.TransactionIdentifier{ + response: &models.TransactionSubmitResponse{ + TransactionIdentifier: &models.TransactionIdentifier{ Hash: "tx1", }, }, err: nil, }, "invalid transaction identifier": { - response: &rosetta.TransactionSubmitResponse{}, - err: errors.New("Transaction.TransactionIdentifier.Hash is missing"), + response: &models.TransactionSubmitResponse{}, + err: errors.New("TransactionIdentifier is nil"), }, } for name, test := range tests { asserter, err := New( context.Background(), - &rosetta.NetworkStatusResponse{ - NetworkStatus: []*rosetta.NetworkStatus{ + &models.NetworkStatusResponse{ + NetworkStatus: []*models.NetworkStatus{ { - NetworkInformation: &rosetta.NetworkInformation{ - GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + NetworkInformation: &models.NetworkInformation{ + GenesisBlockIdentifier: &models.BlockIdentifier{ Index: 0, }, }, }, }, - Options: &rosetta.Options{ - OperationStatuses: []*rosetta.OperationStatus{ + Options: &models.Options{ + OperationStatuses: []*models.OperationStatus{ { Status: "SUCCESS", Successful: true, diff --git a/asserter/mempool.go b/asserter/mempool.go index 0abafa77..2b5caead 100644 --- a/asserter/mempool.go +++ b/asserter/mempool.go @@ -15,15 +15,15 @@ package asserter import ( - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // MempoolTransactions returns an error if any -// rosetta.TransactionIdentifier returns is missing a hash. +// models.TransactionIdentifier returns is missing a hash. // The correctness of each populated MempoolTransaction is // asserted by Transaction. func MempoolTransactions( - transactions []*rosetta.TransactionIdentifier, + transactions []*models.TransactionIdentifier, ) error { for _, t := range transactions { if err := TransactionIdentifier(t); err != nil { diff --git a/asserter/network.go b/asserter/network.go index f9780df9..6515bdd2 100644 --- a/asserter/network.go +++ b/asserter/network.go @@ -18,11 +18,11 @@ import ( "errors" "fmt" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) -// SubNetworkIdentifier asserts a rosetta.SubNetworkIdentifer is valid (if not nil). -func SubNetworkIdentifier(subNetworkIdentifier *rosetta.SubNetworkIdentifier) error { +// SubNetworkIdentifier asserts a models.SubNetworkIdentifer is valid (if not nil). +func SubNetworkIdentifier(subNetworkIdentifier *models.SubNetworkIdentifier) error { if subNetworkIdentifier == nil { return nil } @@ -34,11 +34,15 @@ func SubNetworkIdentifier(subNetworkIdentifier *rosetta.SubNetworkIdentifier) er return nil } -// NetworkIdentifier ensures a rosetta.NetworkIdentifier has +// NetworkIdentifier ensures a models.NetworkIdentifier has // a valid blockchain and network. -func NetworkIdentifier(network *rosetta.NetworkIdentifier) error { - if network == nil || network.Blockchain == "" { - return errors.New("NetworkIdentifier.Blockchain is missing") +func NetworkIdentifier(network *models.NetworkIdentifier) error { + if network == nil { + return errors.New("NetworkIdentifier is nil") + } + + if network.Blockchain == "" { + return errors.New("NetworkIdentifier is nil") } if network.Network == "" { @@ -48,8 +52,8 @@ func NetworkIdentifier(network *rosetta.NetworkIdentifier) error { return SubNetworkIdentifier(network.SubNetworkIdentifier) } -// Peer ensures a rosetta.Peer has a valid peer_id. -func Peer(peer *rosetta.Peer) error { +// Peer ensures a models.Peer has a valid peer_id. +func Peer(peer *models.Peer) error { if peer == nil || peer.PeerID == "" { return errors.New("Peer.PeerID is missing") } @@ -59,7 +63,7 @@ func Peer(peer *rosetta.Peer) error { // Version ensures the version of the node is // returned. -func Version(version *rosetta.Version) error { +func Version(version *models.Version) error { if version == nil { return errors.New("version is nil") } @@ -91,9 +95,9 @@ func StringArray(arrName string, arr []string) error { return nil } -// NetworkInformation ensures any rosetta.NetworkInformation -// included in rosetta.NetworkStatus or rosetta.SubNetworkStatus is valid. -func NetworkInformation(networkInformation *rosetta.NetworkInformation) error { +// NetworkInformation ensures any models.NetworkInformation +// included in models.NetworkStatus or models.SubNetworkStatus is valid. +func NetworkInformation(networkInformation *models.NetworkInformation) error { if networkInformation == nil { return errors.New("network information is nil") } @@ -119,8 +123,8 @@ func NetworkInformation(networkInformation *rosetta.NetworkInformation) error { return nil } -// NetworkStatus ensures a rosetta.NetworkStatus object is valid. -func NetworkStatus(networkStatus *rosetta.NetworkStatus) error { +// NetworkStatus ensures a models.NetworkStatus object is valid. +func NetworkStatus(networkStatus *models.NetworkStatus) error { if networkStatus == nil { return errors.New("network status is nil") } @@ -138,7 +142,7 @@ func NetworkStatus(networkStatus *rosetta.NetworkStatus) error { // OperationStatuses ensures all items in Options.OperationStatuses // are valid and that there exists at least 1 successful status. -func OperationStatuses(statuses []*rosetta.OperationStatus) error { +func OperationStatuses(statuses []*models.OperationStatus) error { if len(statuses) == 0 { return errors.New("no Options.OperationStatuses found") } @@ -161,8 +165,8 @@ func OperationStatuses(statuses []*rosetta.OperationStatus) error { return nil } -// Error ensures a rosetta.Error is valid. -func Error(err *rosetta.Error) error { +// Error ensures a models.Error is valid. +func Error(err *models.Error) error { if err == nil { return errors.New("Error is nil") } @@ -178,9 +182,9 @@ func Error(err *rosetta.Error) error { return nil } -// Errors ensures each rosetta.Error in a slice is valid +// Errors ensures each models.Error in a slice is valid // and that there is no error code collision. -func Errors(rosettaErrors []*rosetta.Error) error { +func Errors(rosettaErrors []*models.Error) error { statusCodes := map[int32]struct{}{} for _, rosettaError := range rosettaErrors { @@ -199,8 +203,8 @@ func Errors(rosettaErrors []*rosetta.Error) error { return nil } -// NetworkOptions ensures a rosetta.Options object is valid. -func NetworkOptions(options *rosetta.Options) error { +// NetworkOptions ensures a models.Options object is valid. +func NetworkOptions(options *models.Options) error { if options == nil { return errors.New("options is nil") } @@ -221,8 +225,8 @@ func NetworkOptions(options *rosetta.Options) error { } // NetworkStatusResponse orchestrates assertions for all -// components of a rosetta.NetworkStatus. -func NetworkStatusResponse(response *rosetta.NetworkStatusResponse) error { +// components of a models.NetworkStatus. +func NetworkStatusResponse(response *models.NetworkStatusResponse) error { for _, network := range response.NetworkStatus { if err := NetworkStatus(network); err != nil { return err diff --git a/asserter/network_test.go b/asserter/network_test.go index 04e5197b..1777b6ee 100644 --- a/asserter/network_test.go +++ b/asserter/network_test.go @@ -18,18 +18,18 @@ import ( "errors" "testing" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" "github.com/stretchr/testify/assert" ) func TestNetworkIdentifier(t *testing.T) { var tests = map[string]struct { - network *rosetta.NetworkIdentifier + network *models.NetworkIdentifier err error }{ "valid network": { - network: &rosetta.NetworkIdentifier{ + network: &models.NetworkIdentifier{ Blockchain: "bitcoin", Network: "mainnet", }, @@ -37,37 +37,37 @@ func TestNetworkIdentifier(t *testing.T) { }, "nil network": { network: nil, - err: errors.New("NetworkIdentifier.Blockchain is missing"), + err: errors.New("NetworkIdentifier is nil"), }, "invalid blockchain": { - network: &rosetta.NetworkIdentifier{ + network: &models.NetworkIdentifier{ Blockchain: "", Network: "mainnet", }, - err: errors.New("NetworkIdentifier.Blockchain is missing"), + err: errors.New("NetworkIdentifier is nil"), }, "invalid network": { - network: &rosetta.NetworkIdentifier{ + network: &models.NetworkIdentifier{ Blockchain: "bitcoin", Network: "", }, err: errors.New("NetworkIdentifier.Network is missing"), }, "valid sub_network": { - network: &rosetta.NetworkIdentifier{ + network: &models.NetworkIdentifier{ Blockchain: "bitcoin", Network: "mainnet", - SubNetworkIdentifier: &rosetta.SubNetworkIdentifier{ + SubNetworkIdentifier: &models.SubNetworkIdentifier{ Network: "shard 1", }, }, err: nil, }, "invalid sub_network": { - network: &rosetta.NetworkIdentifier{ + network: &models.NetworkIdentifier{ Blockchain: "bitcoin", Network: "mainnet", - SubNetworkIdentifier: &rosetta.SubNetworkIdentifier{}, + SubNetworkIdentifier: &models.SubNetworkIdentifier{}, }, err: errors.New("NetworkIdentifier.SubNetworkIdentifier.Network is missing"), }, @@ -89,18 +89,18 @@ func TestVersion(t *testing.T) { ) var tests = map[string]struct { - version *rosetta.Version + version *models.Version err error }{ "valid version": { - version: &rosetta.Version{ + version: &models.Version{ RosettaVersion: validRosettaVersion, NodeVersion: "1.0", }, err: nil, }, "valid version with middleware": { - version: &rosetta.Version{ + version: &models.Version{ RosettaVersion: validRosettaVersion, NodeVersion: "1.0", MiddlewareVersion: &middlewareVersion, @@ -108,7 +108,7 @@ func TestVersion(t *testing.T) { err: nil, }, "old RosettaVersion": { - version: &rosetta.Version{ + version: &models.Version{ RosettaVersion: "1.2.2", NodeVersion: "1.0", }, @@ -119,13 +119,13 @@ func TestVersion(t *testing.T) { err: errors.New("version is nil"), }, "invalid NodeVersion": { - version: &rosetta.Version{ + version: &models.Version{ RosettaVersion: validRosettaVersion, }, err: errors.New("Version.NodeVersion is missing"), }, "invalid MiddlewareVersion": { - version: &rosetta.Version{ + version: &models.Version{ RosettaVersion: validRosettaVersion, NodeVersion: "1.0", MiddlewareVersion: &invalidMiddlewareVersion, @@ -144,7 +144,7 @@ func TestVersion(t *testing.T) { func TestNetworkOptions(t *testing.T) { var ( - operationStatuses = []*rosetta.OperationStatus{ + operationStatuses = []*models.OperationStatus{ { Status: "SUCCESS", Successful: true, @@ -161,11 +161,11 @@ func TestNetworkOptions(t *testing.T) { ) var tests = map[string]struct { - networkOptions *rosetta.Options + networkOptions *models.Options err error }{ "valid options": { - networkOptions: &rosetta.Options{ + networkOptions: &models.Options{ OperationStatuses: operationStatuses, OperationTypes: operationTypes, }, @@ -175,14 +175,14 @@ func TestNetworkOptions(t *testing.T) { err: errors.New("options is nil"), }, "no OperationStatuses": { - networkOptions: &rosetta.Options{ + networkOptions: &models.Options{ OperationTypes: operationTypes, }, err: errors.New("no Options.OperationStatuses found"), }, "no successful OperationStatuses": { - networkOptions: &rosetta.Options{ - OperationStatuses: []*rosetta.OperationStatus{ + networkOptions: &models.Options{ + OperationStatuses: []*models.OperationStatus{ operationStatuses[1], }, OperationTypes: operationTypes, @@ -190,7 +190,7 @@ func TestNetworkOptions(t *testing.T) { err: errors.New("no successful Options.OperationStatuses found"), }, "no OperationTypes": { - networkOptions: &rosetta.Options{ + networkOptions: &models.Options{ OperationStatuses: operationStatuses, }, err: errors.New("no Options.OperationTypes found"), @@ -206,11 +206,11 @@ func TestNetworkOptions(t *testing.T) { func TestError(t *testing.T) { var tests = map[string]struct { - rosettaError *rosetta.Error + rosettaError *models.Error err error }{ "valid error": { - rosettaError: &rosetta.Error{ + rosettaError: &models.Error{ Code: 12, Message: "signature invalid", }, @@ -221,14 +221,14 @@ func TestError(t *testing.T) { err: errors.New("Error is nil"), }, "negative code": { - rosettaError: &rosetta.Error{ + rosettaError: &models.Error{ Code: -1, Message: "signature invalid", }, err: errors.New("Error.Code is negative"), }, "empty message": { - rosettaError: &rosetta.Error{ + rosettaError: &models.Error{ Code: 0, }, err: errors.New("Error.Message is missing"), @@ -244,11 +244,11 @@ func TestError(t *testing.T) { func TestErrors(t *testing.T) { var tests = map[string]struct { - rosettaErrors []*rosetta.Error + rosettaErrors []*models.Error err error }{ "valid errors": { - rosettaErrors: []*rosetta.Error{ + rosettaErrors: []*models.Error{ { Code: 0, Message: "error 1", @@ -261,7 +261,7 @@ func TestErrors(t *testing.T) { err: nil, }, "duplicate error codes": { - rosettaErrors: []*rosetta.Error{ + rosettaErrors: []*models.Error{ { Code: 0, Message: "error 1", diff --git a/asserter/request.go b/asserter/request.go new file mode 100644 index 00000000..16ae1caf --- /dev/null +++ b/asserter/request.go @@ -0,0 +1,145 @@ +// 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" + + "github.com/coinbase/rosetta-sdk-go/models" +) + +// AccountBalanceRequest ensures that a models.AccountBalanceRequest +// is well-formatted. +func AccountBalanceRequest(request *models.AccountBalanceRequest) error { + if request == nil { + return errors.New("AccountBalanceRequest is nil") + } + + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + if err := AccountIdentifier(request.AccountIdentifier); err != nil { + return err + } + + if request.BlockIdentifier == nil { + return nil + } + + return PartialBlockIdentifier(request.BlockIdentifier) +} + +// BlockRequest ensures that a models.BlockRequest +// is well-formatted. +func BlockRequest(request *models.BlockRequest) error { + if request == nil { + return errors.New("BlockRequest is nil") + } + + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + return PartialBlockIdentifier(request.BlockIdentifier) +} + +// BlockTransactionRequest ensures that a models.BlockTransactionRequest +// is well-formatted. +func BlockTransactionRequest(request *models.BlockTransactionRequest) error { + if request == nil { + return errors.New("BlockTransactionRequest is nil") + } + + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + if err := BlockIdentifier(request.BlockIdentifier); err != nil { + return err + } + + return TransactionIdentifier(request.TransactionIdentifier) +} + +// TransactionConstructionRequest ensures that a models.TransactionConstructionRequest +// is well-formatted. +func TransactionConstructionRequest(request *models.TransactionConstructionRequest) error { + if request == nil { + return errors.New("TransactionConstructionRequest is nil") + } + + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + if err := AccountIdentifier(request.AccountIdentifier); err != nil { + return err + } + + if request.Method != nil && *request.Method == "" { + return errors.New("TransactionConstructionRequest.Method is empty") + } + + return nil +} + +// TransactionSubmitRequest ensures that a models.TransactionSubmitRequest +// is well-formatted. +func TransactionSubmitRequest(request *models.TransactionSubmitRequest) error { + if request == nil { + return errors.New("TransactionSubmitRequest is nil") + } + + if request.SignedTransaction == "" { + return errors.New("TransactionSubmitRequest.SignedTransaction is empty") + } + + return nil +} + +// MempoolRequest ensures that a models.MempoolRequest +// is well-formatted. +func MempoolRequest(request *models.MempoolRequest) error { + if request == nil { + return errors.New("MempoolRequest is nil") + } + + return NetworkIdentifier(request.NetworkIdentifier) +} + +// MempoolTransactionRequest ensures that a models.MempoolTransactionRequest +// is well-formatted. +func MempoolTransactionRequest(request *models.MempoolTransactionRequest) error { + if request == nil { + return errors.New("MempoolTransactionRequest is nil") + } + + if err := NetworkIdentifier(request.NetworkIdentifier); err != nil { + return err + } + + return TransactionIdentifier(request.TransactionIdentifier) +} + +// NetworkStatusRequest ensures that a models.NetworkStatusRequest +// is well-formatted. +func NetworkStatusRequest(request *models.NetworkStatusRequest) error { + if request == nil { + return errors.New("NetworkStatusRequest is nil") + } + + return nil +} diff --git a/asserter/request_test.go b/asserter/request_test.go new file mode 100644 index 00000000..baae5624 --- /dev/null +++ b/asserter/request_test.go @@ -0,0 +1,364 @@ +// 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/models" + + "github.com/stretchr/testify/assert" +) + +var ( + validNetworkIdentifier = &models.NetworkIdentifier{ + Blockchain: "Bitcoin", + Network: "Mainnet", + } + + validAccountIdentifier = &models.AccountIdentifier{ + Address: "acct1", + } + + validBlockIndex = int64(1000) + validPartialBlockIdentifier = &models.PartialBlockIdentifier{ + Index: &validBlockIndex, + } + + validBlockIdentifier = &models.BlockIdentifier{ + Index: validBlockIndex, + Hash: "block 1", + } + + validTransactionIdentifier = &models.TransactionIdentifier{ + Hash: "tx1", + } + + validMethod = "transfer" +) + +func TestAccountBalanceRequest(t *testing.T) { + var tests = map[string]struct { + request *models.AccountBalanceRequest + err error + }{ + "valid request": { + request: &models.AccountBalanceRequest{ + NetworkIdentifier: validNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("AccountBalanceRequest is nil"), + }, + "missing network": { + request: &models.AccountBalanceRequest{ + AccountIdentifier: validAccountIdentifier, + }, + err: errors.New("NetworkIdentifier is nil"), + }, + "missing account": { + request: &models.AccountBalanceRequest{ + NetworkIdentifier: validNetworkIdentifier, + }, + err: errors.New("Account is nil"), + }, + "valid historical request": { + request: &models.AccountBalanceRequest{ + NetworkIdentifier: validNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + BlockIdentifier: validPartialBlockIdentifier, + }, + err: nil, + }, + "invalid historical request": { + request: &models.AccountBalanceRequest{ + NetworkIdentifier: validNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + BlockIdentifier: &models.PartialBlockIdentifier{}, + }, + err: errors.New("neither PartialBlockIdentifier.Hash nor PartialBlockIdentifier.Index is set"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := AccountBalanceRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestBlockRequest(t *testing.T) { + var tests = map[string]struct { + request *models.BlockRequest + err error + }{ + "valid request": { + request: &models.BlockRequest{ + NetworkIdentifier: validNetworkIdentifier, + BlockIdentifier: validPartialBlockIdentifier, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("BlockRequest is nil"), + }, + "missing network": { + request: &models.BlockRequest{ + BlockIdentifier: validPartialBlockIdentifier, + }, + err: errors.New("NetworkIdentifier is nil"), + }, + "missing block identifier": { + request: &models.BlockRequest{ + NetworkIdentifier: validNetworkIdentifier, + }, + err: errors.New("PartialBlockIdentifier is nil"), + }, + "invalid PartialBlockIdentifier request": { + request: &models.BlockRequest{ + NetworkIdentifier: validNetworkIdentifier, + BlockIdentifier: &models.PartialBlockIdentifier{}, + }, + err: errors.New("neither PartialBlockIdentifier.Hash nor PartialBlockIdentifier.Index is set"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := BlockRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestBlockTransactionRequest(t *testing.T) { + var tests = map[string]struct { + request *models.BlockTransactionRequest + err error + }{ + "valid request": { + request: &models.BlockTransactionRequest{ + NetworkIdentifier: validNetworkIdentifier, + BlockIdentifier: validBlockIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("BlockTransactionRequest is nil"), + }, + "missing network": { + request: &models.BlockTransactionRequest{ + BlockIdentifier: validBlockIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: errors.New("NetworkIdentifier is nil"), + }, + "missing block identifier": { + request: &models.BlockTransactionRequest{ + NetworkIdentifier: validNetworkIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: errors.New("BlockIdentifier is nil"), + }, + "invalid BlockIdentifier request": { + request: &models.BlockTransactionRequest{ + NetworkIdentifier: validNetworkIdentifier, + BlockIdentifier: &models.BlockIdentifier{}, + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := BlockTransactionRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestTransactionConstructionRequest(t *testing.T) { + var tests = map[string]struct { + request *models.TransactionConstructionRequest + err error + }{ + "valid request": { + request: &models.TransactionConstructionRequest{ + NetworkIdentifier: validNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + }, + err: nil, + }, + "valid request with method": { + request: &models.TransactionConstructionRequest{ + NetworkIdentifier: validNetworkIdentifier, + AccountIdentifier: validAccountIdentifier, + Method: &validMethod, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("TransactionConstructionRequest is nil"), + }, + "missing network": { + request: &models.TransactionConstructionRequest{ + AccountIdentifier: validAccountIdentifier, + }, + err: errors.New("NetworkIdentifier is nil"), + }, + "missing account identifier": { + request: &models.TransactionConstructionRequest{ + NetworkIdentifier: validNetworkIdentifier, + }, + err: errors.New("Account is nil"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := TransactionConstructionRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestTransactionSubmitRequest(t *testing.T) { + var tests = map[string]struct { + request *models.TransactionSubmitRequest + err error + }{ + "valid request": { + request: &models.TransactionSubmitRequest{ + SignedTransaction: "tx", + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("TransactionSubmitRequest is nil"), + }, + "empty tx": { + request: &models.TransactionSubmitRequest{}, + err: errors.New("TransactionSubmitRequest.SignedTransaction is empty"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := TransactionSubmitRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestMempoolRequest(t *testing.T) { + var tests = map[string]struct { + request *models.MempoolRequest + err error + }{ + "valid request": { + request: &models.MempoolRequest{ + NetworkIdentifier: validNetworkIdentifier, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("MempoolRequest is nil"), + }, + "empty tx": { + request: &models.MempoolRequest{}, + err: errors.New("NetworkIdentifier is nil"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := MempoolRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestMempoolTransactionRequest(t *testing.T) { + var tests = map[string]struct { + request *models.MempoolTransactionRequest + err error + }{ + "valid request": { + request: &models.MempoolTransactionRequest{ + NetworkIdentifier: validNetworkIdentifier, + TransactionIdentifier: validTransactionIdentifier, + }, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("MempoolTransactionRequest is nil"), + }, + "missing network": { + request: &models.MempoolTransactionRequest{ + TransactionIdentifier: validTransactionIdentifier, + }, + err: errors.New("NetworkIdentifier is nil"), + }, + "invalid TransactionIdentifier request": { + request: &models.MempoolTransactionRequest{ + NetworkIdentifier: validNetworkIdentifier, + TransactionIdentifier: &models.TransactionIdentifier{}, + }, + err: errors.New("TransactionIdentifier.Hash is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := MempoolTransactionRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} + +func TestNetworkStatusRequest(t *testing.T) { + var tests = map[string]struct { + request *models.NetworkStatusRequest + err error + }{ + "valid request": { + request: &models.NetworkStatusRequest{}, + err: nil, + }, + "nil request": { + request: nil, + err: errors.New("NetworkStatusRequest is nil"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := NetworkStatusRequest(test.request) + assert.Equal(t, test.err, err) + }) + } +} diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..1c4d8c27 --- /dev/null +++ b/client/README.md @@ -0,0 +1,18 @@ +# Client + +[![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/client?tab=overview) + +The Client package reduces the work required to communicate with a Rosetta server. + +If you want a higher-level interface that automatically asserts that server responses +are correct, check out the [Fetcher](/fetcher). + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/client +``` + +## Examples +Check out the [examples](/examples) to see how easy +it is to connect to a Rosetta server. diff --git a/gen/api_account.go b/client/api_account.go similarity index 83% rename from gen/api_account.go rename to client/api_account.go index 098d9baa..192c68a5 100644 --- a/gen/api_account.go +++ b/client/api_account.go @@ -14,12 +14,15 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( _context "context" + "fmt" _ioutil "io/ioutil" _nethttp "net/http" + + models "github.com/coinbase/rosetta-sdk-go/models" ) // Linger please @@ -40,8 +43,8 @@ type AccountAPIService service // optional BlockIdentifier. func (a *AccountAPIService) AccountBalance( ctx _context.Context, - accountBalanceRequest AccountBalanceRequest, -) (*AccountBalanceResponse, *_nethttp.Response, error) { + accountBalanceRequest *models.AccountBalanceRequest, +) (*models.AccountBalanceResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -68,7 +71,7 @@ func (a *AccountAPIService) AccountBalance( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &accountBalanceRequest + localVarPostBody = accountBalanceRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -77,32 +80,30 @@ func (a *AccountAPIService) AccountBalance( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v AccountBalanceResponse + var v models.AccountBalanceResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } diff --git a/gen/api_block.go b/client/api_block.go similarity index 82% rename from gen/api_block.go rename to client/api_block.go index fe2bddb5..44368ee1 100644 --- a/gen/api_block.go +++ b/client/api_block.go @@ -14,12 +14,15 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( _context "context" + "fmt" _ioutil "io/ioutil" _nethttp "net/http" + + models "github.com/coinbase/rosetta-sdk-go/models" ) // Linger please @@ -36,8 +39,8 @@ type BlockAPIService service // be done to get all transaction information. func (a *BlockAPIService) Block( ctx _context.Context, - blockRequest BlockRequest, -) (*BlockResponse, *_nethttp.Response, error) { + blockRequest *models.BlockRequest, +) (*models.BlockResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -64,7 +67,7 @@ func (a *BlockAPIService) Block( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &blockRequest + localVarPostBody = blockRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -73,34 +76,32 @@ func (a *BlockAPIService) Block( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v BlockResponse + var v models.BlockResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } // BlockTransaction Get a transaction in a block by its Transaction Identifier. This method should @@ -117,8 +118,8 @@ func (a *BlockAPIService) Block( // /data directory (on a path that does not conflict with the node). func (a *BlockAPIService) BlockTransaction( ctx _context.Context, - blockTransactionRequest BlockTransactionRequest, -) (*BlockTransactionResponse, *_nethttp.Response, error) { + blockTransactionRequest *models.BlockTransactionRequest, +) (*models.BlockTransactionResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -145,7 +146,7 @@ func (a *BlockAPIService) BlockTransaction( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &blockTransactionRequest + localVarPostBody = blockTransactionRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -154,32 +155,30 @@ func (a *BlockAPIService) BlockTransaction( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v BlockTransactionResponse + var v models.BlockTransactionResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } diff --git a/gen/api_construction.go b/client/api_construction.go similarity index 80% rename from gen/api_construction.go rename to client/api_construction.go index 03f3b81b..81837606 100644 --- a/gen/api_construction.go +++ b/client/api_construction.go @@ -14,12 +14,15 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( _context "context" + "fmt" _ioutil "io/ioutil" _nethttp "net/http" + + models "github.com/coinbase/rosetta-sdk-go/models" ) // Linger please @@ -38,8 +41,8 @@ type ConstructionAPIService service // not need to be broken out into a key-value mapping and can be returned as a blob. func (a *ConstructionAPIService) TransactionConstruction( ctx _context.Context, - transactionConstructionRequest TransactionConstructionRequest, -) (*TransactionConstructionResponse, *_nethttp.Response, error) { + transactionConstructionRequest *models.TransactionConstructionRequest, +) (*models.TransactionConstructionResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -66,7 +69,7 @@ func (a *ConstructionAPIService) TransactionConstruction( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &transactionConstructionRequest + localVarPostBody = transactionConstructionRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -75,34 +78,32 @@ func (a *ConstructionAPIService) TransactionConstruction( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v TransactionConstructionResponse + var v models.TransactionConstructionResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } // TransactionSubmit Submit a pre-signed transaction to the node. This call should not block on the @@ -112,8 +113,8 @@ func (a *ConstructionAPIService) TransactionConstruction( // Otherwise, it should return an error. func (a *ConstructionAPIService) TransactionSubmit( ctx _context.Context, - transactionSubmitRequest TransactionSubmitRequest, -) (*TransactionSubmitResponse, *_nethttp.Response, error) { + transactionSubmitRequest *models.TransactionSubmitRequest, +) (*models.TransactionSubmitResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -140,7 +141,7 @@ func (a *ConstructionAPIService) TransactionSubmit( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &transactionSubmitRequest + localVarPostBody = transactionSubmitRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -149,32 +150,30 @@ func (a *ConstructionAPIService) TransactionSubmit( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v TransactionSubmitResponse + var v models.TransactionSubmitResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } diff --git a/gen/api_mempool.go b/client/api_mempool.go similarity index 79% rename from gen/api_mempool.go rename to client/api_mempool.go index 01ee755f..809a96e1 100644 --- a/gen/api_mempool.go +++ b/client/api_mempool.go @@ -14,12 +14,15 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( _context "context" + "fmt" _ioutil "io/ioutil" _nethttp "net/http" + + models "github.com/coinbase/rosetta-sdk-go/models" ) // Linger please @@ -33,8 +36,8 @@ type MempoolAPIService service // Mempool Get all Transaction Identifiers in the mempool func (a *MempoolAPIService) Mempool( ctx _context.Context, - mempoolRequest MempoolRequest, -) (*MempoolResponse, *_nethttp.Response, error) { + mempoolRequest *models.MempoolRequest, +) (*models.MempoolResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -61,7 +64,7 @@ func (a *MempoolAPIService) Mempool( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &mempoolRequest + localVarPostBody = mempoolRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -70,34 +73,32 @@ func (a *MempoolAPIService) Mempool( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v MempoolResponse + var v models.MempoolResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } // MempoolTransaction Get a transaction in the mempool by its Transaction Identifier. This is a @@ -109,8 +110,8 @@ func (a *MempoolAPIService) Mempool( // in a block. func (a *MempoolAPIService) MempoolTransaction( ctx _context.Context, - mempoolTransactionRequest MempoolTransactionRequest, -) (*MempoolTransactionResponse, *_nethttp.Response, error) { + mempoolTransactionRequest *models.MempoolTransactionRequest, +) (*models.MempoolTransactionResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -137,7 +138,7 @@ func (a *MempoolAPIService) MempoolTransaction( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &mempoolTransactionRequest + localVarPostBody = mempoolTransactionRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -146,32 +147,30 @@ func (a *MempoolAPIService) MempoolTransaction( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v MempoolTransactionResponse + var v models.MempoolTransactionResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } diff --git a/gen/api_network.go b/client/api_network.go similarity index 81% rename from gen/api_network.go rename to client/api_network.go index 9412338e..f323cd62 100644 --- a/gen/api_network.go +++ b/client/api_network.go @@ -14,12 +14,15 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( _context "context" + "fmt" _ioutil "io/ioutil" _nethttp "net/http" + + models "github.com/coinbase/rosetta-sdk-go/models" ) // Linger please @@ -34,8 +37,8 @@ type NetworkAPIService service // method also returns the methods, operation types, and operation statuses the node supports. func (a *NetworkAPIService) NetworkStatus( ctx _context.Context, - networkStatusRequest NetworkStatusRequest, -) (*NetworkStatusResponse, *_nethttp.Response, error) { + networkStatusRequest *models.NetworkStatusRequest, +) (*models.NetworkStatusResponse, *models.Error, error) { var ( localVarPostBody interface{} ) @@ -62,7 +65,7 @@ func (a *NetworkAPIService) NetworkStatus( localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } // body params - localVarPostBody = &networkStatusRequest + localVarPostBody = networkStatusRequest r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) if err != nil { @@ -71,32 +74,30 @@ func (a *NetworkAPIService) NetworkStatus( localVarHTTPResponse, err := a.client.callAPI(ctx, r) if err != nil || localVarHTTPResponse == nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) defer localVarHTTPResponse.Body.Close() if err != nil { - return nil, localVarHTTPResponse, err + return nil, nil, err } if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err } - return nil, localVarHTTPResponse, newErr + + return nil, &v, fmt.Errorf("%+v", v) } - var v NetworkStatusResponse + var v models.NetworkStatusResponse err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return nil, localVarHTTPResponse, newErr + return nil, nil, err } - return &v, localVarHTTPResponse, nil + return &v, nil, nil } diff --git a/gen/client.go b/client/client.go similarity index 93% rename from gen/client.go rename to client/client.go index 577d7f85..b5bbebe0 100644 --- a/gen/client.go +++ b/client/client.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( "bytes" @@ -293,25 +293,3 @@ func detectContentType(body interface{}) string { return contentType } - -// GenericOpenAPIError Provides access to the body, error and model on returned errors. -type GenericOpenAPIError struct { - body []byte - error string - model interface{} -} - -// Error returns non-empty string if there was an error. -func (e GenericOpenAPIError) Error() string { - return e.error -} - -// Body returns the raw bytes of the response -func (e GenericOpenAPIError) Body() []byte { - return e.body -} - -// Model returns the unpacked model of the error -func (e GenericOpenAPIError) Model() interface{} { - return e.model -} diff --git a/gen/configuration.go b/client/configuration.go similarity index 99% rename from gen/configuration.go rename to client/configuration.go index 7c6f4344..e617d568 100644 --- a/gen/configuration.go +++ b/client/configuration.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( "fmt" diff --git a/gen/response.go b/client/response.go similarity index 99% rename from gen/response.go rename to client/response.go index ee928efa..51497211 100644 --- a/gen/response.go +++ b/client/response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package client import ( "net/http" diff --git a/codegen.sh b/codegen.sh index 0538909e..516e9a23 100755 --- a/codegen.sh +++ b/codegen.sh @@ -29,54 +29,96 @@ case "${OS}" in ;; esac -# Remove existing generated code -rm -rf gen; +# Remove existing clienterated code +rm -rf models; +rm -rf client; +rm -rf server; -# Generate new code +# Generate client + models code docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local openapitools/openapi-generator-cli generate \ -i /local/spec.json \ -g go \ - -t /local/templates \ - --additional-properties packageName=gen \ - -o /local/gen; - -# Remove unnecessary files -mv gen/README.md .; -mv -n gen/go.mod .; -rm gen/go.mod; -rm gen/go.sum; -rm -rf gen/api; -rm -rf gen/docs; -rm gen/git_push.sh; -rm gen/.travis.yml; -rm gen/.gitignore; -rm gen/.openapi-generator-ignore; -rm -rf gen/.openapi-generator; + -t /local/templates/client \ + --additional-properties packageName=client\ + -o /local/client; + +# Remove unnecessary client files +rm client/go.mod; +rm client/README.md; +rm client/go.mod; +rm client/go.sum; +rm -rf client/api; +rm -rf client/docs; +rm client/git_push.sh; +rm client/.travis.yml; +rm client/.gitignore; +rm client/.openapi-generator-ignore; +rm -rf client/.openapi-generator; + +# Add server code +docker run --user "$(id -u):$(id -g)" --rm -v "${PWD}":/local openapitools/openapi-generator-cli generate \ + -i /local/spec.json \ + -g go-server \ + -t /local/templates/server \ + --additional-properties packageName=server\ + -o /local/server; + +# Remove unnecessary server files +rm -rf server/api; +rm -rf server/.openapi-generator; +rm server/.openapi-generator-ignore; +rm server/go.mod; +rm server/main.go; +rm server/README.md; +rm server/Dockerfile; +mv server/go/* server/.; +rm -rf server/go; +rm server/model_*.go +rm server/*_service.go + # Fix linting issues -sed "${SED_IFLAG[@]}" 's/Api/API/g' gen/*; -sed "${SED_IFLAG[@]}" 's/Json/JSON/g' gen/*; -sed "${SED_IFLAG[@]}" 's/Id /ID /g' gen/*; -sed "${SED_IFLAG[@]}" 's/Url/URL/g' gen/*; +sed "${SED_IFLAG[@]}" 's/Api/API/g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/Json/JSON/g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/Id /ID /g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/Url/URL/g' client/* server/*; # Remove special characters -sed "${SED_IFLAG[@]}" 's/`//g' gen/*; -sed "${SED_IFLAG[@]}" 's/\"//g' gen/*; -sed "${SED_IFLAG[@]}" 's/\<b>//g' gen/*; -sed "${SED_IFLAG[@]}" 's/\<\/b>//g' gen/*; -sed "${SED_IFLAG[@]}" 's///g' gen/*; -sed "${SED_IFLAG[@]}" 's/<\/code>//g' gen/*; +sed "${SED_IFLAG[@]}" 's/`//g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/\"//g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/\<b>//g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/\<\/b>//g' client/* server/*; +sed "${SED_IFLAG[@]}" 's///g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/<\/code>//g' client/* server/*; # Fix slice containing pointers -sed "${SED_IFLAG[@]}" 's/\*\[\]/\[\]\*/g' gen/*; +sed "${SED_IFLAG[@]}" 's/\*\[\]/\[\]\*/g' client/* server/*; # Fix misspellings -sed "${SED_IFLAG[@]}" 's/occured/occurred/g' gen/*; -sed "${SED_IFLAG[@]}" 's/cannonical/canonical/g' gen/*; -sed "${SED_IFLAG[@]}" 's/Cannonical/Canonical/g' gen/*; +sed "${SED_IFLAG[@]}" 's/occured/occurred/g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/cannonical/canonical/g' client/* server/*; +sed "${SED_IFLAG[@]}" 's/Cannonical/Canonical/g' client/* server/*; + + +# Move model files to models/ +mkdir models; +mv client/model_*.go models/; +for file in models/model_*.go; do + mv "$file" "${file/model_/}" +done + +# Change model files to correct package +sed "${SED_IFLAG[@]}" 's/package client/package models/g' models/*; + +# Format clienterated code +gofmt -w models/; +gofmt -w client/; +gofmt -w server/; -# Format generated code -gofmt -w gen/; +# Copy in READMEs +cp templates/docs/models.md models/README.md; +cp templates/docs/client.md client/README.md; +cp templates/docs/server.md server/README.md; # Ensure license correct make add-license; diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 00000000..b9b0ae5b --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,10 @@ +.PHONY: client fetcher server + +client: + go run client/main.go; + +fetcher: + go run fetcher/main.go; + +server: + go run server/main.go; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..aa967522 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,10 @@ +# Examples + +This folder demonstrates how to write a Rosetta server and how +to use either the Client package or Fetcher package to communicate +with that server. + +## Steps +1. Run `make server` +2. Run `make client` (in a new terminal window) +2. Run `make fetcher` (in a new terminal window) diff --git a/examples/client/main.go b/examples/client/main.go new file mode 100644 index 00000000..46d29128 --- /dev/null +++ b/examples/client/main.go @@ -0,0 +1,138 @@ +// 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 main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/client" + "github.com/coinbase/rosetta-sdk-go/models" +) + +const ( + // serverURL is the URL of a Rosetta Server. + serverURL = "http://localhost:8080" + + // agent is the user-agent on requests to the + // Rosetta Server. + agent = "rosetta-sdk-go" + + // defaultTimeout is the default timeout for + // HTTP requests. + defaultTimeout = 10 * time.Second +) + +func main() { + ctx := context.Background() + + // Step 1: Create a client + clientCfg := client.NewConfiguration( + serverURL, + agent, + &http.Client{ + Timeout: defaultTimeout, + }, + ) + + client := client.NewAPIClient(clientCfg) + + // Step 2: Fetch the network status + networkStatusResponse, rosettaErr, err := client.NetworkAPI.NetworkStatus( + ctx, + &models.NetworkStatusRequest{}, + ) + if rosettaErr != nil { + log.Printf("Rosetta Error: %+v\n", rosettaErr) + } + if err != nil { + log.Fatal(err) + } + + // Step 3: Print the response + prettyNetworkStatusResponse, err := json.MarshalIndent(networkStatusResponse, "", " ") + if err != nil { + log.Fatal(err) + } + log.Printf("Network Status Response: %s\n", string(prettyNetworkStatusResponse)) + + // Step 4: Assert the response is valid + err = asserter.NetworkStatusResponse(networkStatusResponse) + if err != nil { + log.Fatalf("Assertion Error: %s\n", err.Error()) + } + + // Step 5: Create an asserter using the NetworkStatusResponse + // + // This will be used later to assert that a fetched block is + // valid. + asserter, err := asserter.New( + ctx, + networkStatusResponse, + ) + if err != nil { + log.Fatal(err) + } + + // Step 6: Fetch the current block + primaryNetwork := networkStatusResponse.NetworkStatus[0] + block, rosettaErr, err := client.BlockAPI.Block( + ctx, + &models.BlockRequest{ + NetworkIdentifier: primaryNetwork.NetworkIdentifier, + BlockIdentifier: &models.PartialBlockIdentifier{ + Index: &primaryNetwork.NetworkInformation.CurrentBlockIdentifier.Index, + }, + }, + ) + if rosettaErr != nil { + log.Printf("Rosetta Error: %+v\n", rosettaErr) + } + if err != nil { + log.Fatal(err) + } + + // Step 7: Print the block + prettyBlock, err := json.MarshalIndent(block.Block, "", " ") + if err != nil { + log.Fatal(err) + } + log.Printf("Current Block: %s\n", string(prettyBlock)) + + // Step 8: Assert the block response is valid + // + // It is important to note that this only ensures + // required fields are populated and that operations + // in the block only use types and statuses that were + // provided in the networkStatusResponse. To run more + // intensive validation, use the Rosetta Validator. It + // can be found at: https://github.com/coinbase/rosetta-validator + err = asserter.Block(block.Block) + if err != nil { + log.Fatalf("Assertion Error: %s\n", err.Error()) + } + + // Step 9: Print remaining transactions to fetch + // + // If you want the client to automatically fetch these, consider + // using the fetcher package. + for _, txn := range block.OtherTransactions { + log.Printf("Other Transaction: %+v\n", txn) + } +} diff --git a/examples/fetcher/main.go b/examples/fetcher/main.go new file mode 100644 index 00000000..5999d0e2 --- /dev/null +++ b/examples/fetcher/main.go @@ -0,0 +1,133 @@ +// 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 main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/coinbase/rosetta-sdk-go/fetcher" +) + +const ( + // serverURL is the URL of a Rosetta Server. + serverURL = "http://localhost:8080" + + // agent is the user-agent on requests to the + // Rosetta Server. + agent = "rosetta-sdk-go" + + // defaultTimeout is the default timeout for + // HTTP requests. + defaultTimeout = 5 * time.Second + + // blockConcurrency is the number of blocks to + // fetch concurrently in a call to fetcher.SyncBlockRange. + blockConcurrency = 4 + + // transactionConcurrency is the number of transactions to + // fetch concurrently (if BlockResponse.OtherTransactions + // is populated) in a call to fetcher.SyncBlockRange. + transactionConcurrency = 4 + + // maxElapsedTime is the maximum amount of time we will + // spend retrying failed requests. + maxElapsedTime = 1 * time.Minute + + // maxRetries is the maximum number of times we will + // retry a failed request. + maxRetries = 10 +) + +func main() { + ctx := context.Background() + + // Step 1: Create a new fetcher + newFetcher := fetcher.New( + ctx, + serverURL, + agent, + &http.Client{ + Timeout: defaultTimeout, + }, + blockConcurrency, + transactionConcurrency, + ) + + // Step 2: Initialize the fetcher's asserter + // + // Behind the scenes this makes a call to get the + // network status and uses the response to inform + // the asserter what are valid responses. + networkStatusResponse, err := newFetcher.InitializeAsserter(ctx) + if err != nil { + log.Fatal(err) + } + + // Step 3: Print the network status response + prettyNetworkStatusResponse, err := json.MarshalIndent( + networkStatusResponse, + "", + " ", + ) + if err != nil { + log.Fatal(err) + } + log.Printf("Network Status Response: %s\n", string(prettyNetworkStatusResponse)) + + // Step 4: Fetch the current block with retries (automatically + // asserted for correctness) + // + // It is important to note that this assertion only ensures + // required fields are populated and that operations + // in the block only use types and statuses that were + // provided in the networkStatusResponse. To run more + // intensive validation, use the Rosetta Validator. It + // can be found at: https://github.com/coinbase/rosetta-validator + // + // On another note, notice that fetcher.BlockRetry + // automatically fetches all transactions that are + // returned in BlockResponse.OtherTransactions. If you use + // the client directly, you will need to implement a mechanism + // to fully populate the block by fetching all these + // transactions. + primaryNetwork := networkStatusResponse.NetworkStatus[0] + block, err := newFetcher.BlockRetry( + ctx, + primaryNetwork.NetworkIdentifier, + fetcher.PartialBlockIdentifier( + primaryNetwork.NetworkInformation.CurrentBlockIdentifier, + ), + maxElapsedTime, + maxRetries, + ) + if err != nil { + log.Fatal(err) + } + + // Step 5: Print the block + prettyBlock, err := json.MarshalIndent( + block, + "", + " ", + ) + if err != nil { + log.Fatal(err) + } + log.Printf("Current Block: %s\n", string(prettyBlock)) +} diff --git a/examples/server/main.go b/examples/server/main.go new file mode 100644 index 00000000..f63dc00d --- /dev/null +++ b/examples/server/main.go @@ -0,0 +1,52 @@ +// 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 main + +import ( + "fmt" + "log" + "net/http" + + "github.com/coinbase/rosetta-sdk-go/examples/server/services" + "github.com/coinbase/rosetta-sdk-go/models" + "github.com/coinbase/rosetta-sdk-go/server" +) + +const ( + serverPort = 8080 +) + +// NewBlockchainRouter creates a Mux http.Handler from a collection +// of server controllers. +func NewBlockchainRouter(network *models.NetworkIdentifier) http.Handler { + networkAPIService := services.NewNetworkAPIService(network) + networkAPIController := server.NewNetworkAPIController(networkAPIService) + + blockAPIService := services.NewBlockAPIService(network) + blockAPIController := server.NewBlockAPIController(blockAPIService) + + return server.NewRouter(networkAPIController, blockAPIController) +} + +func main() { + network := &models.NetworkIdentifier{ + Blockchain: "Rosetta", + Network: "Testnet", + } + + router := NewBlockchainRouter(network) + log.Printf("Listening on port %d\n", serverPort) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), router)) +} diff --git a/examples/server/services/block_service.go b/examples/server/services/block_service.go new file mode 100644 index 00000000..542c2106 --- /dev/null +++ b/examples/server/services/block_service.go @@ -0,0 +1,136 @@ +// 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 services + +import ( + "github.com/coinbase/rosetta-sdk-go/models" + "github.com/coinbase/rosetta-sdk-go/server" +) + +// BlockAPIService implements the server.BlockAPIServicer interface. +type BlockAPIService struct { + network *models.NetworkIdentifier +} + +// NewBlockAPIService creates a new instance of a BlockAPIService. +func NewBlockAPIService(network *models.NetworkIdentifier) server.BlockAPIServicer { + return &BlockAPIService{ + network: network, + } +} + +// Block implements the /block endpoint. +func (s *BlockAPIService) Block( + request *models.BlockRequest, +) (*models.BlockResponse, *models.Error) { + return &models.BlockResponse{ + Block: &models.Block{ + BlockIdentifier: &models.BlockIdentifier{ + Index: 1000, + Hash: "block 1000", + }, + ParentBlockIdentifier: &models.BlockIdentifier{ + Index: 999, + Hash: "block 999", + }, + Timestamp: 1586483189000, + Transactions: []*models.Transaction{ + { + TransactionIdentifier: &models.TransactionIdentifier{ + Hash: "transaction 0", + }, + Operations: []*models.Operation{ + { + OperationIdentifier: &models.OperationIdentifier{ + Index: 0, + }, + Type: "Transfer", + Status: "Success", + Account: &models.AccountIdentifier{ + Address: "account 0", + }, + Amount: &models.Amount{ + Value: "-1000", + Currency: &models.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + { + OperationIdentifier: &models.OperationIdentifier{ + Index: 1, + }, + RelatedOperations: []*models.OperationIdentifier{ + { + Index: 0, + }, + }, + Type: "Transfer", + Status: "Reverted", + Account: &models.AccountIdentifier{ + Address: "account 1", + }, + Amount: &models.Amount{ + Value: "1000", + Currency: &models.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + }, + }, + }, + }, + OtherTransactions: []*models.TransactionIdentifier{ + { + Hash: "transaction 1", + }, + }, + }, nil +} + +// BlockTransaction implements the /block/transaction endpoint. +func (s *BlockAPIService) BlockTransaction( + request *models.BlockTransactionRequest, +) (*models.BlockTransactionResponse, *models.Error) { + return &models.BlockTransactionResponse{ + Transaction: &models.Transaction{ + TransactionIdentifier: &models.TransactionIdentifier{ + Hash: "transaction 1", + }, + Operations: []*models.Operation{ + { + OperationIdentifier: &models.OperationIdentifier{ + Index: 0, + }, + Type: "Reward", + Status: "Success", + Account: &models.AccountIdentifier{ + Address: "account 2", + }, + Amount: &models.Amount{ + Value: "1000", + Currency: &models.Currency{ + Symbol: "ROS", + Decimals: 2, + }, + }, + }, + }, + }, + }, nil +} diff --git a/examples/server/services/network_service.go b/examples/server/services/network_service.go new file mode 100644 index 00000000..172e68b2 --- /dev/null +++ b/examples/server/services/network_service.go @@ -0,0 +1,87 @@ +// 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 services + +import ( + "github.com/coinbase/rosetta-sdk-go/models" + "github.com/coinbase/rosetta-sdk-go/server" +) + +// NetworkAPIService implements the server.NetworkAPIServicer interface. +type NetworkAPIService struct { + network *models.NetworkIdentifier +} + +// NewNetworkAPIService creates a new instance of a NetworkAPIService. +func NewNetworkAPIService(network *models.NetworkIdentifier) server.NetworkAPIServicer { + return &NetworkAPIService{ + network: network, + } +} + +// NetworkStatus implements the /network/status endpoint. +func (s *NetworkAPIService) NetworkStatus( + *models.NetworkStatusRequest, +) (*models.NetworkStatusResponse, *models.Error) { + return &models.NetworkStatusResponse{ + NetworkStatus: []*models.NetworkStatus{ + { + NetworkIdentifier: s.network, + NetworkInformation: &models.NetworkInformation{ + CurrentBlockIdentifier: &models.BlockIdentifier{ + Index: 1000, + Hash: "block 1000", + }, + CurrentBlockTimestamp: int64(1586483189000), + GenesisBlockIdentifier: &models.BlockIdentifier{ + Index: 0, + Hash: "block 0", + }, + Peers: []*models.Peer{ + { + PeerID: "peer 1", + }, + }, + }, + }, + }, + Version: &models.Version{ + RosettaVersion: "1.3.0", + NodeVersion: "0.0.1", + }, + Options: &models.Options{ + OperationStatuses: []*models.OperationStatus{ + { + Status: "Success", + Successful: true, + }, + { + Status: "Reverted", + Successful: false, + }, + }, + OperationTypes: []string{ + "Transfer", + "Reward", + }, + Errors: []*models.Error{ + { + Code: 1, + Message: "not implemented", + }, + }, + }, + }, nil +} diff --git a/fetcher/README.md b/fetcher/README.md new file mode 100644 index 00000000..12dbb1be --- /dev/null +++ b/fetcher/README.md @@ -0,0 +1,19 @@ +# Fetcher + +[![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/fetcher?tab=overview) + +The Fetcher package provides a simplified client interface to communicate +with a Rosetta server. + +If you want a lower-level interface to communicate with a Rosetta server, +check out the [Client](/client). + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/fetcher +``` + +## Examples +Check out the [examples](/examples) to see how easy +it is to connect to a Rosetta server. diff --git a/fetcher/account.go b/fetcher/account.go index 395ec18c..74f66f02 100644 --- a/fetcher/account.go +++ b/fetcher/account.go @@ -22,18 +22,18 @@ import ( "github.com/coinbase/rosetta-sdk-go/asserter" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // UnsafeAccountBalance returns the unvalidated response // from the AccountBalance method. func (f *Fetcher) UnsafeAccountBalance( ctx context.Context, - network *rosetta.NetworkIdentifier, - account *rosetta.AccountIdentifier, -) (*rosetta.BlockIdentifier, []*rosetta.Balance, error) { + network *models.NetworkIdentifier, + account *models.AccountIdentifier, +) (*models.BlockIdentifier, []*models.Balance, error) { balance, _, err := f.rosettaClient.AccountAPI.AccountBalance(ctx, - rosetta.AccountBalanceRequest{ + &models.AccountBalanceRequest{ NetworkIdentifier: network, AccountIdentifier: account, }, @@ -49,9 +49,9 @@ func (f *Fetcher) UnsafeAccountBalance( // from the AccountBalance method. func (f *Fetcher) AccountBalance( ctx context.Context, - network *rosetta.NetworkIdentifier, - account *rosetta.AccountIdentifier, -) (*rosetta.BlockIdentifier, []*rosetta.Balance, error) { + network *models.NetworkIdentifier, + account *models.AccountIdentifier, +) (*models.BlockIdentifier, []*models.Balance, error) { block, balances, err := f.UnsafeAccountBalance(ctx, network, account) if err != nil { return nil, nil, err @@ -68,11 +68,11 @@ func (f *Fetcher) AccountBalance( // with a specified number of retries and max elapsed time. func (f *Fetcher) AccountBalanceRetry( ctx context.Context, - network *rosetta.NetworkIdentifier, - account *rosetta.AccountIdentifier, + network *models.NetworkIdentifier, + account *models.AccountIdentifier, maxElapsedTime time.Duration, maxRetries uint64, -) (*rosetta.BlockIdentifier, []*rosetta.Balance, error) { +) (*models.BlockIdentifier, []*models.Balance, error) { backoffRetries := backoffRetries(maxElapsedTime, maxRetries) for ctx.Err() == nil { diff --git a/fetcher/block.go b/fetcher/block.go index 89e85992..0143479c 100644 --- a/fetcher/block.go +++ b/fetcher/block.go @@ -20,19 +20,20 @@ import ( "fmt" "time" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" "golang.org/x/sync/errgroup" ) // addTransactionIdentifiers appends a slice of -// rosetta.TransactionIdentifiers to a channel. -// When all rosetta.TransactionIdentifiers are added, +// models.TransactionIdentifiers to a channel. +// When all models.TransactionIdentifiers are added, // the channel is closed. func addTransactionIdentifiers( ctx context.Context, - txsToFetch chan *rosetta.TransactionIdentifier, - identifiers []*rosetta.TransactionIdentifier, + txsToFetch chan *models.TransactionIdentifier, + identifiers []*models.TransactionIdentifier, ) error { defer close(txsToFetch) for _, txHash := range identifiers { @@ -51,14 +52,14 @@ func addTransactionIdentifiers( // channel or there is an error. func (f *Fetcher) fetchChannelTransactions( ctx context.Context, - network *rosetta.NetworkIdentifier, - block *rosetta.BlockIdentifier, - txsToFetch chan *rosetta.TransactionIdentifier, - fetchedTxs chan *rosetta.Transaction, + network *models.NetworkIdentifier, + block *models.BlockIdentifier, + txsToFetch chan *models.TransactionIdentifier, + fetchedTxs chan *models.Transaction, ) error { for transactionIdentifier := range txsToFetch { tx, _, err := f.rosettaClient.BlockAPI.BlockTransaction(ctx, - rosetta.BlockTransactionRequest{ + &models.BlockTransactionRequest{ NetworkIdentifier: network, BlockIdentifier: block, TransactionIdentifier: transactionIdentifier, @@ -81,22 +82,22 @@ func (f *Fetcher) fetchChannelTransactions( // UnsafeTransactions returns the unvalidated response // from the BlockTransaction method. UnsafeTransactions -// fetches all provided rosetta.TransactionIdentifiers +// fetches all provided models.TransactionIdentifiers // concurrently (with the number of threads specified // by txConcurrency). If any fetch fails, this function // will return an error. func (f *Fetcher) UnsafeTransactions( ctx context.Context, - network *rosetta.NetworkIdentifier, - block *rosetta.BlockIdentifier, - transactionIdentifiers []*rosetta.TransactionIdentifier, -) ([]*rosetta.Transaction, error) { + network *models.NetworkIdentifier, + block *models.BlockIdentifier, + transactionIdentifiers []*models.TransactionIdentifier, +) ([]*models.Transaction, error) { if len(transactionIdentifiers) == 0 { return nil, nil } - txsToFetch := make(chan *rosetta.TransactionIdentifier) - fetchedTxs := make(chan *rosetta.Transaction) + txsToFetch := make(chan *models.TransactionIdentifier) + fetchedTxs := make(chan *models.Transaction) g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return addTransactionIdentifiers(ctx, txsToFetch, transactionIdentifiers) @@ -113,7 +114,7 @@ func (f *Fetcher) UnsafeTransactions( close(fetchedTxs) }() - txs := make([]*rosetta.Transaction, 0) + txs := make([]*models.Transaction, 0) for tx := range fetchedTxs { txs = append(txs, tx) } @@ -132,10 +133,10 @@ func (f *Fetcher) UnsafeTransactions( // block. func (f *Fetcher) UnsafeBlock( ctx context.Context, - network *rosetta.NetworkIdentifier, - blockIdentifier *rosetta.PartialBlockIdentifier, -) (*rosetta.Block, error) { - blockResponse, _, err := f.rosettaClient.BlockAPI.Block(ctx, rosetta.BlockRequest{ + network *models.NetworkIdentifier, + blockIdentifier *models.PartialBlockIdentifier, +) (*models.Block, error) { + blockResponse, _, err := f.rosettaClient.BlockAPI.Block(ctx, &models.BlockRequest{ NetworkIdentifier: network, BlockIdentifier: blockIdentifier, }) @@ -170,9 +171,9 @@ func (f *Fetcher) UnsafeBlock( // block. func (f *Fetcher) Block( ctx context.Context, - network *rosetta.NetworkIdentifier, - blockIdentifier *rosetta.PartialBlockIdentifier, -) (*rosetta.Block, error) { + network *models.NetworkIdentifier, + blockIdentifier *models.PartialBlockIdentifier, +) (*models.Block, error) { if f.Asserter == nil { return nil, errors.New("asserter not initialized") } @@ -182,7 +183,7 @@ func (f *Fetcher) Block( return nil, err } - if err := f.Asserter.Block(ctx, block); err != nil { + if err := f.Asserter.Block(block); err != nil { return nil, err } @@ -193,15 +194,19 @@ func (f *Fetcher) Block( // with a specified number of retries and max elapsed time. func (f *Fetcher) BlockRetry( ctx context.Context, - network *rosetta.NetworkIdentifier, - blockIdentifier *rosetta.PartialBlockIdentifier, + network *models.NetworkIdentifier, + blockIdentifier *models.PartialBlockIdentifier, maxElapsedTime time.Duration, maxRetries uint64, -) (*rosetta.Block, error) { +) (*models.Block, error) { if f.Asserter == nil { return nil, errors.New("asserter not initialized") } + if err := asserter.PartialBlockIdentifier(blockIdentifier); err != nil { + return nil, err + } + backoffRetries := backoffRetries(maxElapsedTime, maxRetries) for ctx.Err() == nil { @@ -214,7 +219,14 @@ func (f *Fetcher) BlockRetry( return block, nil } - if !tryAgain(fmt.Sprintf("block %d", blockIdentifier.Index), backoffRetries, err) { + var blockFetchErr string + if blockIdentifier.Index != nil { + blockFetchErr = fmt.Sprintf("block %d", *blockIdentifier.Index) + } else { + blockFetchErr = fmt.Sprintf("block %s", *blockIdentifier.Hash) + } + + if !tryAgain(blockFetchErr, backoffRetries, err) { break } } @@ -225,7 +237,7 @@ func (f *Fetcher) BlockRetry( // BlockAndLatency is utilized to track the latency // of concurrent block fetches. type BlockAndLatency struct { - Block *rosetta.Block + Block *models.Block Latency float64 } @@ -256,7 +268,7 @@ func addBlockIndicies( // error. func (f *Fetcher) fetchChannelBlocks( ctx context.Context, - network *rosetta.NetworkIdentifier, + network *models.NetworkIdentifier, blockIndicies chan int64, results chan *BlockAndLatency, ) error { @@ -265,7 +277,7 @@ func (f *Fetcher) fetchChannelBlocks( block, err := f.BlockRetry( ctx, network, - &rosetta.PartialBlockIdentifier{ + &models.PartialBlockIdentifier{ Index: &b, }, DefaultElapsedTime, @@ -295,7 +307,7 @@ func (f *Fetcher) fetchChannelBlocks( // by any callers. func (f *Fetcher) BlockRange( ctx context.Context, - network *rosetta.NetworkIdentifier, + network *models.NetworkIdentifier, startIndex int64, endIndex int64, ) (map[int64]*BlockAndLatency, error) { diff --git a/fetcher/construction.go b/fetcher/construction.go index 2d9c33d2..3055edf6 100644 --- a/fetcher/construction.go +++ b/fetcher/construction.go @@ -20,19 +20,19 @@ import ( "github.com/coinbase/rosetta-sdk-go/asserter" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // ConstructionMetadata returns the validated response // from the ConstructionMetadata method. func (f *Fetcher) ConstructionMetadata( ctx context.Context, - network *rosetta.NetworkIdentifier, - account *rosetta.AccountIdentifier, + network *models.NetworkIdentifier, + account *models.AccountIdentifier, method *string, -) (*rosetta.Amount, *map[string]interface{}, error) { +) (*models.Amount, *map[string]interface{}, error) { metadata, _, err := f.rosettaClient.ConstructionAPI.TransactionConstruction(ctx, - rosetta.TransactionConstructionRequest{ + &models.TransactionConstructionRequest{ NetworkIdentifier: network, AccountIdentifier: account, Method: method, @@ -54,14 +54,14 @@ func (f *Fetcher) ConstructionMetadata( func (f *Fetcher) ConstructionSubmit( ctx context.Context, signedTransaction string, -) (*rosetta.TransactionIdentifier, *map[string]interface{}, error) { +) (*models.TransactionIdentifier, *map[string]interface{}, error) { if f.Asserter == nil { return nil, nil, errors.New("asserter not initialized") } submitResponse, _, err := f.rosettaClient.ConstructionAPI.TransactionSubmit( ctx, - rosetta.TransactionSubmitRequest{ + &models.TransactionSubmitRequest{ SignedTransaction: signedTransaction, }, ) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 3841a3b6..ee8e2c53 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -20,8 +20,8 @@ import ( "time" "github.com/coinbase/rosetta-sdk-go/asserter" - - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/client" + "github.com/coinbase/rosetta-sdk-go/models" ) const ( @@ -49,10 +49,10 @@ const ( type Fetcher struct { // Asserter is a public variable because // it can be used to determine if a retrieved - // rosetta.Operation is successful and should + // models.Operation is successful and should // be applied. Asserter *asserter.Asserter - rosettaClient *rosetta.APIClient + rosettaClient *client.APIClient blockConcurrency uint64 transactionConcurrency uint64 } @@ -66,8 +66,8 @@ func New( blockConcurrency uint64, transactionConcurrency uint64, ) *Fetcher { - clientCfg := rosetta.NewConfiguration(serverAddress, userAgent, httpClient) - client := rosetta.NewAPIClient(clientCfg) + clientCfg := client.NewConfiguration(serverAddress, userAgent, httpClient) + client := client.NewAPIClient(clientCfg) return &Fetcher{ rosettaClient: client, @@ -78,12 +78,12 @@ func New( // InitializeAsserter creates an Asserter for // validating responses. The Asserter is created -// from a rosetta.NetworkStatusResponse. This +// from a models.NetworkStatusResponse. This // method should be called before making any // validated client requests. func (f *Fetcher) InitializeAsserter( ctx context.Context, -) (*rosetta.NetworkStatusResponse, error) { +) (*models.NetworkStatusResponse, error) { // Attempt to fetch network status networkResponse, err := f.NetworkStatusRetry( ctx, diff --git a/fetcher/mempool.go b/fetcher/mempool.go index 6a2d8656..c555ec50 100644 --- a/fetcher/mempool.go +++ b/fetcher/mempool.go @@ -20,18 +20,21 @@ import ( "github.com/coinbase/rosetta-sdk-go/asserter" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // UnsafeMempool returns the unvalidated response // from the Mempool method. func (f *Fetcher) UnsafeMempool( ctx context.Context, - network *rosetta.NetworkIdentifier, -) ([]*rosetta.TransactionIdentifier, error) { - mempool, _, err := f.rosettaClient.MempoolAPI.Mempool(ctx, rosetta.MempoolRequest{ - NetworkIdentifier: network, - }) + network *models.NetworkIdentifier, +) ([]*models.TransactionIdentifier, error) { + mempool, _, err := f.rosettaClient.MempoolAPI.Mempool( + ctx, + &models.MempoolRequest{ + NetworkIdentifier: network, + }, + ) if err != nil { return nil, err } @@ -43,8 +46,8 @@ func (f *Fetcher) UnsafeMempool( // from the Mempool method. func (f *Fetcher) Mempool( ctx context.Context, - network *rosetta.NetworkIdentifier, -) ([]*rosetta.TransactionIdentifier, error) { + network *models.NetworkIdentifier, +) ([]*models.TransactionIdentifier, error) { mempool, err := f.UnsafeMempool(ctx, network) if err != nil { return nil, err @@ -61,11 +64,12 @@ func (f *Fetcher) Mempool( // from the MempoolTransaction method. func (f *Fetcher) UnsafeMempoolTransaction( ctx context.Context, - network *rosetta.NetworkIdentifier, - transaction *rosetta.TransactionIdentifier, -) (*rosetta.Transaction, *map[string]interface{}, error) { - mempoolTransaction, _, err := f.rosettaClient.MempoolAPI.MempoolTransaction(ctx, - rosetta.MempoolTransactionRequest{ + network *models.NetworkIdentifier, + transaction *models.TransactionIdentifier, +) (*models.Transaction, *map[string]interface{}, error) { + mempoolTransaction, _, err := f.rosettaClient.MempoolAPI.MempoolTransaction( + ctx, + &models.MempoolTransactionRequest{ NetworkIdentifier: network, TransactionIdentifier: transaction, }, @@ -81,9 +85,9 @@ func (f *Fetcher) UnsafeMempoolTransaction( // from the MempoolTransaction method. func (f *Fetcher) MempoolTransaction( ctx context.Context, - network *rosetta.NetworkIdentifier, - transaction *rosetta.TransactionIdentifier, -) (*rosetta.Transaction, *map[string]interface{}, error) { + network *models.NetworkIdentifier, + transaction *models.TransactionIdentifier, +) (*models.Transaction, *map[string]interface{}, error) { if f.Asserter == nil { return nil, nil, errors.New("asserter not initialized") } diff --git a/fetcher/network.go b/fetcher/network.go index 027d0ac5..ab9a31a2 100644 --- a/fetcher/network.go +++ b/fetcher/network.go @@ -21,7 +21,7 @@ import ( "github.com/coinbase/rosetta-sdk-go/asserter" - rosetta "github.com/coinbase/rosetta-sdk-go/gen" + "github.com/coinbase/rosetta-sdk-go/models" ) // UnsafeNetworkStatus returns the unvalidated response @@ -29,10 +29,10 @@ import ( func (f *Fetcher) UnsafeNetworkStatus( ctx context.Context, metadata *map[string]interface{}, -) (*rosetta.NetworkStatusResponse, error) { +) (*models.NetworkStatusResponse, error) { networkStatus, _, err := f.rosettaClient.NetworkAPI.NetworkStatus( ctx, - rosetta.NetworkStatusRequest{ + &models.NetworkStatusRequest{ Metadata: metadata, }, ) @@ -48,7 +48,7 @@ func (f *Fetcher) UnsafeNetworkStatus( func (f *Fetcher) NetworkStatus( ctx context.Context, metadata *map[string]interface{}, -) (*rosetta.NetworkStatusResponse, error) { +) (*models.NetworkStatusResponse, error) { networkStatus, err := f.UnsafeNetworkStatus(ctx, metadata) if err != nil { return nil, err @@ -68,7 +68,7 @@ func (f *Fetcher) NetworkStatusRetry( metadata *map[string]interface{}, maxElapsedTime time.Duration, maxRetries uint64, -) (*rosetta.NetworkStatusResponse, error) { +) (*models.NetworkStatusResponse, error) { backoffRetries := backoffRetries(maxElapsedTime, maxRetries) for ctx.Err() == nil { diff --git a/fetcher/utils.go b/fetcher/utils.go index bd7360e4..164f35af 100644 --- a/fetcher/utils.go +++ b/fetcher/utils.go @@ -18,9 +18,23 @@ import ( "log" "time" + "github.com/coinbase/rosetta-sdk-go/models" + "github.com/cenkalti/backoff" ) +// PartialBlockIdentifier constructs a PartialBlockIdentifier +// from a BlockIdentifier. This is useful when making block requests +// with the fetcher. +func PartialBlockIdentifier( + blockIdentifier *models.BlockIdentifier, +) *models.PartialBlockIdentifier { + return &models.PartialBlockIdentifier{ + Hash: &blockIdentifier.Hash, + Index: &blockIdentifier.Index, + } +} + // backoffRetries creates the backoff.BackOff struct used by all // *Retry functions in the fetcher. func backoffRetries( diff --git a/go.mod b/go.mod index 1804b156..74fff0e0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/cenkalti/backoff v2.2.1+incompatible + github.com/gorilla/mux v1.7.4 github.com/stretchr/testify v1.5.1 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a ) diff --git a/go.sum b/go.sum index 2d06817b..6750db72 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/models/README.md b/models/README.md new file mode 100644 index 00000000..30481a27 --- /dev/null +++ b/models/README.md @@ -0,0 +1,13 @@ +# Models + +[![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/models?tab=overview) + +Models contains a collection of auto-generated Rosetta models. Using this +package ensures that you don't need to automatically generate code on your +own. + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/models +``` diff --git a/gen/model_account_balance_request.go b/models/account_balance_request.go similarity index 98% rename from gen/model_account_balance_request.go rename to models/account_balance_request.go index 2c996199..840eb1c9 100644 --- a/gen/model_account_balance_request.go +++ b/models/account_balance_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // AccountBalanceRequest An AccountBalanceRequest is utilized to make a balance request on the // /account/balance endpoint. If the block_identifier is populated, a historical balance query diff --git a/gen/model_account_balance_response.go b/models/account_balance_response.go similarity index 98% rename from gen/model_account_balance_response.go rename to models/account_balance_response.go index 565d919f..aa29a85f 100644 --- a/gen/model_account_balance_response.go +++ b/models/account_balance_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // AccountBalanceResponse An AccountBalanceResponse is returned on the /account/balance endpoint. type AccountBalanceResponse struct { diff --git a/gen/model_account_identifier.go b/models/account_identifier.go similarity index 98% rename from gen/model_account_identifier.go rename to models/account_identifier.go index fe31370e..d0b7c331 100644 --- a/gen/model_account_identifier.go +++ b/models/account_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // AccountIdentifier The account_identifier uniquely identifies an account within a network. All // fields in the account_identifier are utilized to determine this uniqueness (including the diff --git a/gen/model_amount.go b/models/amount.go similarity index 98% rename from gen/model_amount.go rename to models/amount.go index 87013bef..112da0d7 100644 --- a/gen/model_amount.go +++ b/models/amount.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Amount Amount is some Value of a Currency. It is considered invalid to specify a Value without a // Currency. diff --git a/gen/model_balance.go b/models/balance.go similarity index 98% rename from gen/model_balance.go rename to models/balance.go index 26f448e2..cd0c4b2f 100644 --- a/gen/model_balance.go +++ b/models/balance.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Balance Balance is the array of Amount controlled by an AccountIdentifier. An underspecified // AccountIdentifier may result in many amounts (ex: all ERC-20 balances for a single address). diff --git a/gen/model_block.go b/models/block.go similarity index 98% rename from gen/model_block.go rename to models/block.go index cfe13990..e2223be9 100644 --- a/gen/model_block.go +++ b/models/block.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Block Blocks contain an array of Transactions that occurred at a particular BlockIdentifier. type Block struct { diff --git a/gen/model_block_identifier.go b/models/block_identifier.go similarity index 98% rename from gen/model_block_identifier.go rename to models/block_identifier.go index ae5a054d..cea817a7 100644 --- a/gen/model_block_identifier.go +++ b/models/block_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // BlockIdentifier The block_identifier uniquely identifies a block in a particular network. type BlockIdentifier struct { diff --git a/gen/model_block_request.go b/models/block_request.go similarity index 98% rename from gen/model_block_request.go rename to models/block_request.go index 67c2f749..187a32c0 100644 --- a/gen/model_block_request.go +++ b/models/block_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // BlockRequest A BlockRequest is utilized to make a block request on the /block endpoint. type BlockRequest struct { diff --git a/gen/model_block_response.go b/models/block_response.go similarity index 98% rename from gen/model_block_response.go rename to models/block_response.go index bb6143e1..39404718 100644 --- a/gen/model_block_response.go +++ b/models/block_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // BlockResponse A BlockResponse includes a fully-populated block or a partially-populated block // with a list of other transactions to fetch (other_transactions). diff --git a/gen/model_block_transaction_request.go b/models/block_transaction_request.go similarity index 98% rename from gen/model_block_transaction_request.go rename to models/block_transaction_request.go index 970c07c1..a906438b 100644 --- a/gen/model_block_transaction_request.go +++ b/models/block_transaction_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // BlockTransactionRequest A BlockTransactionRequest is used to fetch a Transaction included in a // block that is not returned in a BlockResponse. diff --git a/gen/model_block_transaction_response.go b/models/block_transaction_response.go similarity index 98% rename from gen/model_block_transaction_response.go rename to models/block_transaction_response.go index e0557d9c..b5b846cf 100644 --- a/gen/model_block_transaction_response.go +++ b/models/block_transaction_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // BlockTransactionResponse A BlockTransactionResponse contains information about a block // transaction. diff --git a/gen/model_currency.go b/models/currency.go similarity index 98% rename from gen/model_currency.go rename to models/currency.go index 86e07be6..93776ff5 100644 --- a/gen/model_currency.go +++ b/models/currency.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Currency Currency is composed of a canonical Symbol and Decimals. This Decimals value is used to // convert an Amount.Value from atomic units (Satoshis) to standard units (Bitcoins). diff --git a/gen/model_error.go b/models/error.go similarity index 98% rename from gen/model_error.go rename to models/error.go index 93e52ef7..1f19b77e 100644 --- a/gen/model_error.go +++ b/models/error.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // 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. diff --git a/gen/model_mempool_request.go b/models/mempool_request.go similarity index 98% rename from gen/model_mempool_request.go rename to models/mempool_request.go index 3adc1118..e0ea3a78 100644 --- a/gen/model_mempool_request.go +++ b/models/mempool_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // MempoolRequest A MempoolRequest is utilized to retrieve all transaction identifiers in the // mempool for a particular network_identifier. diff --git a/gen/model_mempool_response.go b/models/mempool_response.go similarity index 98% rename from gen/model_mempool_response.go rename to models/mempool_response.go index b5e93d3f..5905d5c7 100644 --- a/gen/model_mempool_response.go +++ b/models/mempool_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // MempoolResponse A MempoolResponse contains all transaction identifiers in the mempool for a // particular network_identifier. diff --git a/gen/model_mempool_transaction_request.go b/models/mempool_transaction_request.go similarity index 98% rename from gen/model_mempool_transaction_request.go rename to models/mempool_transaction_request.go index fe376247..5f46e11d 100644 --- a/gen/model_mempool_transaction_request.go +++ b/models/mempool_transaction_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // MempoolTransactionRequest A MempoolTransactionRequest is utilized to retrieve a transaction from // the mempool. diff --git a/gen/model_mempool_transaction_response.go b/models/mempool_transaction_response.go similarity index 98% rename from gen/model_mempool_transaction_response.go rename to models/mempool_transaction_response.go index d19094ab..6cb83f04 100644 --- a/gen/model_mempool_transaction_response.go +++ b/models/mempool_transaction_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // MempoolTransactionResponse A MempoolTransactionResponse contains an estimate of a mempool // transaction. It may not be possible to know the full impact of a transaction in the mempool (ex: diff --git a/gen/model_network_identifier.go b/models/network_identifier.go similarity index 98% rename from gen/model_network_identifier.go rename to models/network_identifier.go index f3fa19da..bdf6322a 100644 --- a/gen/model_network_identifier.go +++ b/models/network_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // NetworkIdentifier The network_identifier specifies which network a particular object is // associated with. diff --git a/gen/model_network_information.go b/models/network_information.go similarity index 98% rename from gen/model_network_information.go rename to models/network_information.go index 1e9840dc..e53dd90a 100644 --- a/gen/model_network_information.go +++ b/models/network_information.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // NetworkInformation NetworkInformation contains basic information about a node's view of a // blockchain network. diff --git a/gen/model_network_status.go b/models/network_status.go similarity index 98% rename from gen/model_network_status.go rename to models/network_status.go index 69380b66..41174f51 100644 --- a/gen/model_network_status.go +++ b/models/network_status.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // NetworkStatus A NetworkStatus object contains the network_information pertaining to a network // specified by the network_identifier. diff --git a/gen/model_network_status_request.go b/models/network_status_request.go similarity index 98% rename from gen/model_network_status_request.go rename to models/network_status_request.go index 1c0a21df..95457b89 100644 --- a/gen/model_network_status_request.go +++ b/models/network_status_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // NetworkStatusRequest A NetworkStatusRequest is utilized to retrieve the status of the network. It // currently contains only optional metadata. diff --git a/gen/model_network_status_response.go b/models/network_status_response.go similarity index 98% rename from gen/model_network_status_response.go rename to models/network_status_response.go index 7f7f1be0..23d21ec8 100644 --- a/gen/model_network_status_response.go +++ b/models/network_status_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // NetworkStatusResponse A NetworkStatusResponse contains all information required to make reliable // requests to the Rosetta Server implementation. diff --git a/gen/model_operation.go b/models/operation.go similarity index 99% rename from gen/model_operation.go rename to models/operation.go index 9a4972aa..d8574b2c 100644 --- a/gen/model_operation.go +++ b/models/operation.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Operation Operations contain all balance-changing information within a transaction. They are // always one-sided (only affect 1 AccountIdentifier) and can succeed or fail independently from a diff --git a/gen/model_operation_identifier.go b/models/operation_identifier.go similarity index 98% rename from gen/model_operation_identifier.go rename to models/operation_identifier.go index c6092fac..ef4b5011 100644 --- a/gen/model_operation_identifier.go +++ b/models/operation_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // OperationIdentifier The operation_identifier uniquely identifies an operation within a // transaction. diff --git a/gen/model_operation_status.go b/models/operation_status.go similarity index 98% rename from gen/model_operation_status.go rename to models/operation_status.go index a699bdb7..2a877d4c 100644 --- a/gen/model_operation_status.go +++ b/models/operation_status.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // OperationStatus OperationStatus is utilized to indicate which Operation status are considered // successful. diff --git a/gen/model_options.go b/models/options.go similarity index 99% rename from gen/model_options.go rename to models/options.go index d810d5f3..d42b95e2 100644 --- a/gen/model_options.go +++ b/models/options.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Options Options specify supported Operation status, Operation types, and all possible error // statuses. This Options object is used by clients to validate the correctness of a Rosetta Server diff --git a/gen/model_partial_block_identifier.go b/models/partial_block_identifier.go similarity index 98% rename from gen/model_partial_block_identifier.go rename to models/partial_block_identifier.go index 79ff65a5..dc2eeca7 100644 --- a/gen/model_partial_block_identifier.go +++ b/models/partial_block_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // PartialBlockIdentifier When fetching data by BlockIdentifier, it may be possible to only specify // the index or hash. If neither property is specified, it is assumed that the client is making a diff --git a/gen/model_peer.go b/models/peer.go similarity index 98% rename from gen/model_peer.go rename to models/peer.go index 2fe4d15f..76ca169c 100644 --- a/gen/model_peer.go +++ b/models/peer.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Peer A Peer is a representation of a node's peer. type Peer struct { diff --git a/gen/model_sub_account_identifier.go b/models/sub_account_identifier.go similarity index 98% rename from gen/model_sub_account_identifier.go rename to models/sub_account_identifier.go index 1c1e43fc..52160df7 100644 --- a/gen/model_sub_account_identifier.go +++ b/models/sub_account_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // SubAccountIdentifier An account may have state specific to a contract address (ERC-20 token) // and/or a stake (delegated balance). The sub_account_identifier should specify which state (if diff --git a/gen/model_sub_network_identifier.go b/models/sub_network_identifier.go similarity index 98% rename from gen/model_sub_network_identifier.go rename to models/sub_network_identifier.go index 979755bc..649ac7f3 100644 --- a/gen/model_sub_network_identifier.go +++ b/models/sub_network_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // SubNetworkIdentifier In blockchains with sharded state, the SubNetworkIdentifier is required to // query some object on a specific shard. This identifier is optional for all non-sharded diff --git a/gen/model_transaction.go b/models/transaction.go similarity index 98% rename from gen/model_transaction.go rename to models/transaction.go index 44c327a5..47dc23e1 100644 --- a/gen/model_transaction.go +++ b/models/transaction.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Transaction Transactions contain an array of Operations that are attributable to the same // TransactionIdentifier. diff --git a/gen/model_transaction_construction_request.go b/models/transaction_construction_request.go similarity index 99% rename from gen/model_transaction_construction_request.go rename to models/transaction_construction_request.go index 18402706..c4634b93 100644 --- a/gen/model_transaction_construction_request.go +++ b/models/transaction_construction_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // TransactionConstructionRequest A TransactionConstructionRequest is utilized to get information // required to construct a transaction. Optionally, the client can provide a method to reduce the diff --git a/gen/model_transaction_construction_response.go b/models/transaction_construction_response.go similarity index 98% rename from gen/model_transaction_construction_response.go rename to models/transaction_construction_response.go index bc1821f3..81cc8aa6 100644 --- a/gen/model_transaction_construction_response.go +++ b/models/transaction_construction_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // TransactionConstructionResponse The transaction construction response should return the network // fee to use in transaction construction. This network fee should be the gas price or cost per byte diff --git a/gen/model_transaction_identifier.go b/models/transaction_identifier.go similarity index 98% rename from gen/model_transaction_identifier.go rename to models/transaction_identifier.go index 1a374417..2395e259 100644 --- a/gen/model_transaction_identifier.go +++ b/models/transaction_identifier.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // TransactionIdentifier The transaction_identifier uniquely identifies a transaction in a // particular network and block or in the mempool. diff --git a/gen/model_transaction_submit_request.go b/models/transaction_submit_request.go similarity index 98% rename from gen/model_transaction_submit_request.go rename to models/transaction_submit_request.go index 0cf03cca..2d560c17 100644 --- a/gen/model_transaction_submit_request.go +++ b/models/transaction_submit_request.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // TransactionSubmitRequest The transaction submission request includes a signed transaction. type TransactionSubmitRequest struct { diff --git a/gen/model_transaction_submit_response.go b/models/transaction_submit_response.go similarity index 98% rename from gen/model_transaction_submit_response.go rename to models/transaction_submit_response.go index 27238424..6ab296b1 100644 --- a/gen/model_transaction_submit_response.go +++ b/models/transaction_submit_response.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // TransactionSubmitResponse A TransactionSubmitResponse contains the transaction_identifier of a // submitted transaction that was accepted into the mempool. diff --git a/gen/model_version.go b/models/version.go similarity index 99% rename from gen/model_version.go rename to models/version.go index a844072e..b63c8300 100644 --- a/gen/model_version.go +++ b/models/version.go @@ -14,7 +14,7 @@ // Generated by: OpenAPI Generator (https://openapi-generator.tech) -package gen +package models // Version The Version object is utilized to inform the client of the versions of different // components of the Rosetta implementation. diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..33a93350 --- /dev/null +++ b/server/README.md @@ -0,0 +1,40 @@ +# Server + +[![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/server?tab=overview) + +The Server package reduces the work required to write your own Rosetta server. +In short, this package takes care of the basics (boilerplate server code +and request validation) so that you can focus on code that is unique to your +implementation. + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/server +``` + +## Components +### Router +The router is a [Mux](https://github.com/gorilla/mux) router that +routes traffic to the correct controller. + +### Controller +Contollers are automatically generated code that specify an interface +that a service must implement. + +### Services +Services are implemented by you to populate responses. These services +are invoked by controllers. + +## Recommended Folder Structure +``` +main.go +/services + block_service.go + network_service.go + ... +``` + +## Examples +Check out the [examples](/examples) to see how easy +it is to create your own server. diff --git a/server/api.go b/server/api.go new file mode 100644 index 00000000..50b0a29d --- /dev/null +++ b/server/api.go @@ -0,0 +1,123 @@ +// 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 server + +import ( + "net/http" + + "github.com/coinbase/rosetta-sdk-go/models" +) + +// AccountAPIRouter defines the required methods for binding the api requests to a responses for the +// AccountAPI +// The AccountAPIRouter implementation should parse necessary information from the http request, +// pass the data to a AccountAPIServicer to perform the required actions, then write the service +// results to the http response. +type AccountAPIRouter interface { + AccountBalance(http.ResponseWriter, *http.Request) +} + +// BlockAPIRouter defines the required methods for binding the api requests to a responses for the +// BlockAPI +// The BlockAPIRouter implementation should parse necessary information from the http request, +// pass the data to a BlockAPIServicer to perform the required actions, then write the service +// results to the http response. +type BlockAPIRouter interface { + Block(http.ResponseWriter, *http.Request) + BlockTransaction(http.ResponseWriter, *http.Request) +} + +// ConstructionAPIRouter defines the required methods for binding the api requests to a responses +// for the ConstructionAPI +// The ConstructionAPIRouter implementation should parse necessary information from the http +// request, +// pass the data to a ConstructionAPIServicer to perform the required actions, then write the +// service results to the http response. +type ConstructionAPIRouter interface { + TransactionConstruction(http.ResponseWriter, *http.Request) + TransactionSubmit(http.ResponseWriter, *http.Request) +} + +// MempoolAPIRouter defines the required methods for binding the api requests to a responses for the +// MempoolAPI +// The MempoolAPIRouter implementation should parse necessary information from the http request, +// pass the data to a MempoolAPIServicer to perform the required actions, then write the service +// results to the http response. +type MempoolAPIRouter interface { + Mempool(http.ResponseWriter, *http.Request) + MempoolTransaction(http.ResponseWriter, *http.Request) +} + +// NetworkAPIRouter defines the required methods for binding the api requests to a responses for the +// NetworkAPI +// The NetworkAPIRouter implementation should parse necessary information from the http request, +// pass the data to a NetworkAPIServicer to perform the required actions, then write the service +// results to the http response. +type NetworkAPIRouter interface { + NetworkStatus(http.ResponseWriter, *http.Request) +} + +// AccountAPIServicer defines the api actions for the AccountAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type AccountAPIServicer interface { + AccountBalance(*models.AccountBalanceRequest) (*models.AccountBalanceResponse, *models.Error) +} + +// BlockAPIServicer defines the api actions for the BlockAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type BlockAPIServicer interface { + Block(*models.BlockRequest) (*models.BlockResponse, *models.Error) + BlockTransaction( + *models.BlockTransactionRequest, + ) (*models.BlockTransactionResponse, *models.Error) +} + +// ConstructionAPIServicer defines the api actions for the ConstructionAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type ConstructionAPIServicer interface { + TransactionConstruction( + *models.TransactionConstructionRequest, + ) (*models.TransactionConstructionResponse, *models.Error) + TransactionSubmit( + *models.TransactionSubmitRequest, + ) (*models.TransactionSubmitResponse, *models.Error) +} + +// MempoolAPIServicer defines the api actions for the MempoolAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type MempoolAPIServicer interface { + Mempool(*models.MempoolRequest) (*models.MempoolResponse, *models.Error) + MempoolTransaction( + *models.MempoolTransactionRequest, + ) (*models.MempoolTransactionResponse, *models.Error) +} + +// NetworkAPIServicer defines the api actions for the NetworkAPI service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type NetworkAPIServicer interface { + NetworkStatus(*models.NetworkStatusRequest) (*models.NetworkStatusResponse, *models.Error) +} diff --git a/server/api_account.go b/server/api_account.go new file mode 100644 index 00000000..a3a1bdfb --- /dev/null +++ b/server/api_account.go @@ -0,0 +1,91 @@ +// 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 server + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" +) + +// A AccountAPIController binds http requests to an api service and writes the service results to +// the http response +type AccountAPIController struct { + service AccountAPIServicer +} + +// NewAccountAPIController creates a default api controller +func NewAccountAPIController(s AccountAPIServicer) Router { + return &AccountAPIController{service: s} +} + +// Routes returns all of the api route for the AccountAPIController +func (c *AccountAPIController) Routes() Routes { + return Routes{ + { + "AccountBalance", + strings.ToUpper("Post"), + "/account/balance", + c.AccountBalance, + }, + } +} + +// AccountBalance - Get an Account Balance +func (c *AccountAPIController) AccountBalance(w http.ResponseWriter, r *http.Request) { + accountBalanceRequest := &models.AccountBalanceRequest{} + if err := json.NewDecoder(r.Body).Decode(&accountBalanceRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that AccountBalanceRequest is correct + if err := asserter.AccountBalanceRequest(accountBalanceRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.AccountBalance(accountBalanceRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} diff --git a/server/api_block.go b/server/api_block.go new file mode 100644 index 00000000..347a452b --- /dev/null +++ b/server/api_block.go @@ -0,0 +1,138 @@ +// 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 server + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" +) + +// A BlockAPIController binds http requests to an api service and writes the service results to the +// http response +type BlockAPIController struct { + service BlockAPIServicer +} + +// NewBlockAPIController creates a default api controller +func NewBlockAPIController(s BlockAPIServicer) Router { + return &BlockAPIController{service: s} +} + +// Routes returns all of the api route for the BlockAPIController +func (c *BlockAPIController) Routes() Routes { + return Routes{ + { + "Block", + strings.ToUpper("Post"), + "/block", + c.Block, + }, + { + "BlockTransaction", + strings.ToUpper("Post"), + "/block/transaction", + c.BlockTransaction, + }, + } +} + +// Block - Get a Block +func (c *BlockAPIController) Block(w http.ResponseWriter, r *http.Request) { + blockRequest := &models.BlockRequest{} + if err := json.NewDecoder(r.Body).Decode(&blockRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that BlockRequest is correct + if err := asserter.BlockRequest(blockRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.Block(blockRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} + +// BlockTransaction - Get a Block Transaction +func (c *BlockAPIController) BlockTransaction(w http.ResponseWriter, r *http.Request) { + blockTransactionRequest := &models.BlockTransactionRequest{} + if err := json.NewDecoder(r.Body).Decode(&blockTransactionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that BlockTransactionRequest is correct + if err := asserter.BlockTransactionRequest(blockTransactionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.BlockTransaction(blockTransactionRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} diff --git a/server/api_construction.go b/server/api_construction.go new file mode 100644 index 00000000..4c8d203b --- /dev/null +++ b/server/api_construction.go @@ -0,0 +1,141 @@ +// 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 server + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" +) + +// A ConstructionAPIController binds http requests to an api service and writes the service results +// to the http response +type ConstructionAPIController struct { + service ConstructionAPIServicer +} + +// NewConstructionAPIController creates a default api controller +func NewConstructionAPIController(s ConstructionAPIServicer) Router { + return &ConstructionAPIController{service: s} +} + +// Routes returns all of the api route for the ConstructionAPIController +func (c *ConstructionAPIController) Routes() Routes { + return Routes{ + { + "TransactionConstruction", + strings.ToUpper("Post"), + "/construction/metadata", + c.TransactionConstruction, + }, + { + "TransactionSubmit", + strings.ToUpper("Post"), + "/construction/submit", + c.TransactionSubmit, + }, + } +} + +// TransactionConstruction - Get Transaction Construction Metadata +func (c *ConstructionAPIController) TransactionConstruction( + w http.ResponseWriter, + r *http.Request, +) { + transactionConstructionRequest := &models.TransactionConstructionRequest{} + if err := json.NewDecoder(r.Body).Decode(&transactionConstructionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that TransactionConstructionRequest is correct + if err := asserter.TransactionConstructionRequest(transactionConstructionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.TransactionConstruction(transactionConstructionRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} + +// TransactionSubmit - Submit a Signed Transaction +func (c *ConstructionAPIController) TransactionSubmit(w http.ResponseWriter, r *http.Request) { + transactionSubmitRequest := &models.TransactionSubmitRequest{} + if err := json.NewDecoder(r.Body).Decode(&transactionSubmitRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that TransactionSubmitRequest is correct + if err := asserter.TransactionSubmitRequest(transactionSubmitRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.TransactionSubmit(transactionSubmitRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} diff --git a/server/api_mempool.go b/server/api_mempool.go new file mode 100644 index 00000000..d0fe266b --- /dev/null +++ b/server/api_mempool.go @@ -0,0 +1,138 @@ +// 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 server + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" +) + +// A MempoolAPIController binds http requests to an api service and writes the service results to +// the http response +type MempoolAPIController struct { + service MempoolAPIServicer +} + +// NewMempoolAPIController creates a default api controller +func NewMempoolAPIController(s MempoolAPIServicer) Router { + return &MempoolAPIController{service: s} +} + +// Routes returns all of the api route for the MempoolAPIController +func (c *MempoolAPIController) Routes() Routes { + return Routes{ + { + "Mempool", + strings.ToUpper("Post"), + "/mempool", + c.Mempool, + }, + { + "MempoolTransaction", + strings.ToUpper("Post"), + "/mempool/transaction", + c.MempoolTransaction, + }, + } +} + +// Mempool - Get All Mempool Transactions +func (c *MempoolAPIController) Mempool(w http.ResponseWriter, r *http.Request) { + mempoolRequest := &models.MempoolRequest{} + if err := json.NewDecoder(r.Body).Decode(&mempoolRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that MempoolRequest is correct + if err := asserter.MempoolRequest(mempoolRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.Mempool(mempoolRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} + +// MempoolTransaction - Get a Mempool Transaction +func (c *MempoolAPIController) MempoolTransaction(w http.ResponseWriter, r *http.Request) { + mempoolTransactionRequest := &models.MempoolTransactionRequest{} + if err := json.NewDecoder(r.Body).Decode(&mempoolTransactionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that MempoolTransactionRequest is correct + if err := asserter.MempoolTransactionRequest(mempoolTransactionRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.MempoolTransaction(mempoolTransactionRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} diff --git a/server/api_network.go b/server/api_network.go new file mode 100644 index 00000000..c5bba46c --- /dev/null +++ b/server/api_network.go @@ -0,0 +1,91 @@ +// 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 server + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/asserter" + "github.com/coinbase/rosetta-sdk-go/models" +) + +// A NetworkAPIController binds http requests to an api service and writes the service results to +// the http response +type NetworkAPIController struct { + service NetworkAPIServicer +} + +// NewNetworkAPIController creates a default api controller +func NewNetworkAPIController(s NetworkAPIServicer) Router { + return &NetworkAPIController{service: s} +} + +// Routes returns all of the api route for the NetworkAPIController +func (c *NetworkAPIController) Routes() Routes { + return Routes{ + { + "NetworkStatus", + strings.ToUpper("Post"), + "/network/status", + c.NetworkStatus, + }, + } +} + +// NetworkStatus - Get Network Status +func (c *NetworkAPIController) NetworkStatus(w http.ResponseWriter, r *http.Request) { + networkStatusRequest := &models.NetworkStatusRequest{} + if err := json.NewDecoder(r.Body).Decode(&networkStatusRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that NetworkStatusRequest is correct + if err := asserter.NetworkStatusRequest(networkStatusRequest); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + result, serviceErr := c.service.NetworkStatus(networkStatusRequest) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +} diff --git a/server/logger.go b/server/logger.go new file mode 100644 index 00000000..8d38cb60 --- /dev/null +++ b/server/logger.go @@ -0,0 +1,39 @@ +// 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 server + +import ( + "log" + "net/http" + "time" +) + +func Logger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s %s %s %s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} diff --git a/server/routers.go b/server/routers.go new file mode 100644 index 00000000..c378c170 --- /dev/null +++ b/server/routers.go @@ -0,0 +1,88 @@ +// 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 server + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +// A Route defines the parameters for an api endpoint +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// Routes are a collection of defined api endpoints +type Routes []Route + +// Router defines the required methods for retrieving api routes +type Router interface { + Routes() Routes +} + +// CorsMiddleware handle CORS and ensures OPTIONS requests are +// handled properly. +func CorsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept", + ) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST,OPTIONS") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +// NewRouter creates a new router for any number of api routers +func NewRouter(routers ...Router) http.Handler { + router := mux.NewRouter().StrictSlash(true) + for _, api := range routers { + for _, route := range api.Routes() { + var handler http.Handler + handler = route.HandlerFunc + handler = Logger(handler, route.Name) + + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(handler) + } + } + + // use custom middleware because could not get mux handler to work in Chrome + return CorsMiddleware(router) +} + +// EncodeJSONResponse uses the json encoder to write an interface to the http response with an +// optional status code +func EncodeJSONResponse(i interface{}, status int, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(status) + + return json.NewEncoder(w).Encode(i) +} diff --git a/templates/README.mustache b/templates/README.mustache deleted file mode 100644 index 2f498a90..00000000 --- a/templates/README.mustache +++ /dev/null @@ -1,50 +0,0 @@ -# rosetta-sdk-go - -[![Coinbase](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master.svg?style=shield)](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master) -[![Coverage Status](https://coveralls.io/repos/github/coinbase/rosetta-sdk-go/badge.svg)](https://coveralls.io/github/coinbase/rosetta-sdk-go) -[![Go Report Card](https://goreportcard.com/badge/github.com/coinbase/rosetta-sdk-go)](https://goreportcard.com/report/github.com/coinbase/rosetta-sdk-go) -[![License](https://img.shields.io/github/license/coinbase/rosetta-sdk-go.svg)](https://github.com/coinbase/rosetta-sdk-go/blob/master/LICENSE.txt) -[![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?tab=overview) - -## What is Rosetta? -Rosetta is a new project from Coinbase to standardize the process -of deploying and interacting with blockchains. With an explicit -specification to adhere to, all parties involved in blockchain -development can spend less time figuring out how to integrate -with each other and more time working on the novel advances that -will push the blockchain ecosystem forward. In practice, this means -that any blockchain project that implements the requirements outlined -in this specification will enable exchanges, block explorers, -and wallets to integrate with much less communication overhead -and network-specific work. - -## Versioning -- Rosetta version: {{appVersion}} - -## Installation - -```shell -go get github.com/coinbase/rosetta-sdk-go -``` - -## Automatic Assertion -When using the helper methods to access a Rosetta Server (in `fetcher/*.go`), -responses from the server are automatically checked for adherence to -the Rosetta Interface. For example, if a `BlockIdentifer` is returned without a -`Hash`, the fetch will fail. Take a look at the tests in `asserter/*_test.go` -if you are curious about what exactly is asserted. - -_It is possible, but not recommended, to bypass this assertion using the -`unsafe` helper methods available in `fetcher/*.go`._ - -## Development -* `make deps` to install dependencies -* `make gen` to generate models and helpers -* `make test` to run tests -* `make lint` to lint the source code (included generated code) -* `make release` to check if code passes all tests run by CircleCI - -## License -This project is available open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). - -© 2020 Coinbase diff --git a/templates/api.mustache b/templates/api.mustache deleted file mode 100644 index f9220c17..00000000 --- a/templates/api.mustache +++ /dev/null @@ -1,198 +0,0 @@ -{{>partial_header}} -package {{packageName}} - -{{#operations}} -import ( - _context "context" - _ioutil "io/ioutil" - _nethttp "net/http" -{{#imports}} "{{import}}" -{{/imports}} -) - -// Linger please -var ( - _ _context.Context -) - -// {{classname}}Service {{classname}} service -type {{classname}}Service service -{{#operation}} - -{{#hasOptionalParams}} -// {{#structPrefix}}{{&classname}}{{/structPrefix}}{{{nickname}}}Opts Optional parameters for the method '{{{nickname}}}' -type {{#structPrefix}}{{&classname}}{{/structPrefix}}{{{nickname}}}Opts struct { -{{#allParams}} -{{^required}} -{{#isPrimitiveType}} -{{^isBinary}} - {{vendorExtensions.x-export-param-name}} optional.{{vendorExtensions.x-optional-data-type}} -{{/isBinary}} -{{#isBinary}} - {{vendorExtensions.x-export-param-name}} optional.Interface -{{/isBinary}} -{{/isPrimitiveType}} -{{^isPrimitiveType}} - {{vendorExtensions.x-export-param-name}} optional.Interface -{{/isPrimitiveType}} -{{/required}} -{{/allParams}} -} - -{{/hasOptionalParams}} -// {{operationId}}{{#notes}} {{notes}}{{/notes}} -func (a *{{{classname}}}Service) {{{nickname}}}(ctx _context.Context{{#hasParams}}, {{/hasParams}}{{#allParams}}{{#required}}{{paramName}} {{{dataType}}}{{#hasMore}}, {{/hasMore}}{{/required}}{{/allParams}}{{#hasOptionalParams}}localVarOptionals *{{#structPrefix}}{{&classname}}{{/structPrefix}}{{{nickname}}}Opts{{/hasOptionalParams}}) ({{#returnType}}*{{{returnType}}}, {{/returnType}}*_nethttp.Response, error) { - var ( - localVarPostBody interface{} - ) - - // create path and map variables - localVarPath := a.client.cfg.BasePath + "{{{path}}}"{{#pathParams}} - localVarPath = strings.Replace(localVarPath, "{"+"{{baseName}}"+"}", _neturl.QueryEscape(parameterToString({{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) , -1) - {{/pathParams}} - - localVarHeaderParams := make(map[string]string) - {{#allParams}} - {{#required}} - {{#minItems}} - if len({{paramName}}) < {{minItems}} { - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must have at least {{minItems}} elements") - } - {{/minItems}} - {{#maxItems}} - if len({{paramName}}) > {{maxItems}} { - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must have less than {{maxItems}} elements") - } - {{/maxItems}} - {{#minLength}} - if strlen({{paramName}}) < {{minLength}} { - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must have at least {{minLength}} elements") - } - {{/minLength}} - {{#maxLength}} - if strlen({{paramName}}) > {{maxLength}} { - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must have less than {{maxLength}} elements") - } - {{/maxLength}} - {{#minimum}} - {{#isString}} - {{paramName}}Txt, err := atoi({{paramName}}) - if {{paramName}}Txt < {{minimum}} { - {{/isString}} - {{^isString}} - if {{paramName}} < {{minimum}} { - {{/isString}} - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must be greater than {{minimum}}") - } - {{/minimum}} - {{#maximum}} - {{#isString}} - {{paramName}}Txt, err := atoi({{paramName}}) - if {{paramName}}Txt > {{maximum}} { - {{/isString}} - {{^isString}} - if {{paramName}} > {{maximum}} { - {{/isString}} - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} must be less than {{maximum}}") - } - {{/maximum}} - {{/required}} - {{/allParams}} - - // to determine the Content-Type header -{{=<% %>=}} - localVarHTTPContentTypes := []string{<%#consumes%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/consumes%>} -<%={{ }}=%> - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header -{{=<% %>=}} - localVarHTTPHeaderAccepts := []string{<%#produces%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/produces%>} -<%={{ }}=%> - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } -{{#hasHeaderParams}} -{{#headerParams}} - {{#required}} - localVarHeaderParams["{{baseName}}"] = parameterToString({{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") - {{/required}} - {{^required}} - if localVarOptionals != nil && localVarOptionals.{{vendorExtensions.x-export-param-name}}.IsSet() { - localVarHeaderParams["{{baseName}}"] = parameterToString(localVarOptionals.{{vendorExtensions.x-export-param-name}}.Value(), "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") - } - {{/required}} -{{/headerParams}} -{{/hasHeaderParams}} -{{#hasBodyParam}} -{{#bodyParams}} - // body params -{{#required}} - localVarPostBody = &{{paramName}} -{{/required}} -{{^required}} - if localVarOptionals != nil && localVarOptionals.{{vendorExtensions.x-export-param-name}}.IsSet() { - {{#isPrimitiveType}} - localVarPostBody = localVarOptionals.{{vendorExtensions.x-export-param-name}}.Value() - {{/isPrimitiveType}} - {{^isPrimitiveType}} - localVarOptional{{vendorExtensions.x-export-param-name}}, localVarOptional{{vendorExtensions.x-export-param-name}}ok := localVarOptionals.{{vendorExtensions.x-export-param-name}}.Value().({{{dataType}}}) - if !localVarOptional{{vendorExtensions.x-export-param-name}}ok { - return {{#returnType}}nil, {{/returnType}}nil, reportError("{{paramName}} should be {{dataType}}") - } - localVarPostBody = &localVarOptional{{vendorExtensions.x-export-param-name}} - {{/isPrimitiveType}} - } - -{{/required}} -{{/bodyParams}} -{{/hasBodyParam}} - - r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) - if err != nil { - return {{#returnType}}nil, {{/returnType}}nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(ctx, r) - if err != nil || localVarHTTPResponse == nil { - return {{#returnType}}nil, {{/returnType}}localVarHTTPResponse, err - } - - localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) - defer localVarHTTPResponse.Body.Close() - if err != nil { - return {{#returnType}}nil, {{/returnType}}localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return {{#returnType}}nil, {{/returnType}}localVarHTTPResponse, newErr - } - - {{#returnType}} - var v {{{returnType}}} - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return {{#returnType}}nil, {{/returnType}}localVarHTTPResponse, newErr - } - - {{/returnType}} - return {{#returnType}}&v, {{/returnType}}localVarHTTPResponse, nil -} -{{/operation}} -{{/operations}} diff --git a/templates/client/api.mustache b/templates/client/api.mustache new file mode 100644 index 00000000..5c698cb6 --- /dev/null +++ b/templates/client/api.mustache @@ -0,0 +1,110 @@ +{{>partial_header}} +package {{packageName}} + +{{#operations}} +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" + models "github.com/coinbase/rosetta-sdk-go/models" +{{#imports}} "{{import}}" +{{/imports}} +) + +// Linger please +var ( + _ _context.Context +) + +// {{classname}}Service {{classname}} service +type {{classname}}Service service +{{#operation}} + +// {{operationId}}{{#notes}} {{notes}}{{/notes}} +func (a *{{{classname}}}Service) {{{nickname}}}(ctx _context.Context{{#hasParams}}, {{/hasParams}}{{#allParams}}{{#required}}{{paramName}} *models.{{{dataType}}}{{#hasMore}}, {{/hasMore}}{{/required}}{{/allParams}}{{#hasOptionalParams}}localVarOptionals *{{#structPrefix}}{{&classname}}{{/structPrefix}}{{{nickname}}}Opts{{/hasOptionalParams}}) ({{#returnType}}*models.{{{returnType}}}, {{/returnType}} *models.Error, error) { + var ( + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "{{{path}}}" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header +{{=<% %>=}} + localVarHTTPContentTypes := []string{<%#consumes%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/consumes%>} +<%={{ }}=%> + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header +{{=<% %>=}} + localVarHTTPHeaderAccepts := []string{<%#produces%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/produces%>} +<%={{ }}=%> + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } +{{#hasHeaderParams}} +{{#headerParams}} + {{#required}} + localVarHeaderParams["{{baseName}}"] = parameterToString({{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") + {{/required}} + {{^required}} + if localVarOptionals != nil && localVarOptionals.{{vendorExtensions.x-export-param-name}}.IsSet() { + localVarHeaderParams["{{baseName}}"] = parameterToString(localVarOptionals.{{vendorExtensions.x-export-param-name}}.Value(), "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") + } + {{/required}} +{{/headerParams}} +{{/hasHeaderParams}} +{{#hasBodyParam}} +{{#bodyParams}} + // body params + localVarPostBody = {{paramName}} +{{/bodyParams}} +{{/hasBodyParam}} + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, nil, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, nil, err + } + + if localVarHTTPResponse.StatusCode != _nethttp.StatusOK { + var v models.Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err + } + + return nil, &v, fmt.Errorf("%+v", v) + } + + {{#returnType}} + var v models.{{{returnType}}} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + return nil, nil, err + } + + {{/returnType}} + return &v, nil, nil +} +{{/operation}} +{{/operations}} diff --git a/templates/client.mustache b/templates/client/client.mustache similarity index 92% rename from templates/client.mustache rename to templates/client/client.mustache index 3cb8a293..f9f63f32 100644 --- a/templates/client.mustache +++ b/templates/client/client.mustache @@ -15,7 +15,6 @@ import ( "reflect" "regexp" "strings" - "time" ) const ( @@ -278,25 +277,3 @@ func detectContentType(body interface{}) string { return contentType } - -// GenericOpenAPIError Provides access to the body, error and model on returned errors. -type GenericOpenAPIError struct { - body []byte - error string - model interface{} -} - -// Error returns non-empty string if there was an error. -func (e GenericOpenAPIError) Error() string { - return e.error -} - -// Body returns the raw bytes of the response -func (e GenericOpenAPIError) Body() []byte { - return e.body -} - -// Model returns the unpacked model of the error -func (e GenericOpenAPIError) Model() interface{} { - return e.model -} diff --git a/templates/configuration.mustache b/templates/client/configuration.mustache similarity index 100% rename from templates/configuration.mustache rename to templates/client/configuration.mustache diff --git a/templates/model.mustache b/templates/client/model.mustache similarity index 100% rename from templates/model.mustache rename to templates/client/model.mustache diff --git a/templates/partial_header.mustache b/templates/client/partial_header.mustache similarity index 100% rename from templates/partial_header.mustache rename to templates/client/partial_header.mustache diff --git a/templates/response.mustache b/templates/client/response.mustache similarity index 100% rename from templates/response.mustache rename to templates/client/response.mustache diff --git a/templates/docs/client.md b/templates/docs/client.md new file mode 100644 index 00000000..1c4d8c27 --- /dev/null +++ b/templates/docs/client.md @@ -0,0 +1,18 @@ +# Client + +[![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/client?tab=overview) + +The Client package reduces the work required to communicate with a Rosetta server. + +If you want a higher-level interface that automatically asserts that server responses +are correct, check out the [Fetcher](/fetcher). + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/client +``` + +## Examples +Check out the [examples](/examples) to see how easy +it is to connect to a Rosetta server. diff --git a/templates/docs/models.md b/templates/docs/models.md new file mode 100644 index 00000000..30481a27 --- /dev/null +++ b/templates/docs/models.md @@ -0,0 +1,13 @@ +# Models + +[![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/models?tab=overview) + +Models contains a collection of auto-generated Rosetta models. Using this +package ensures that you don't need to automatically generate code on your +own. + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/models +``` diff --git a/templates/docs/server.md b/templates/docs/server.md new file mode 100644 index 00000000..33a93350 --- /dev/null +++ b/templates/docs/server.md @@ -0,0 +1,40 @@ +# Server + +[![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/server?tab=overview) + +The Server package reduces the work required to write your own Rosetta server. +In short, this package takes care of the basics (boilerplate server code +and request validation) so that you can focus on code that is unique to your +implementation. + +## Installation + +```shell +go get github.com/coinbase/rosetta-sdk-go/server +``` + +## Components +### Router +The router is a [Mux](https://github.com/gorilla/mux) router that +routes traffic to the correct controller. + +### Controller +Contollers are automatically generated code that specify an interface +that a service must implement. + +### Services +Services are implemented by you to populate responses. These services +are invoked by controllers. + +## Recommended Folder Structure +``` +main.go +/services + block_service.go + network_service.go + ... +``` + +## Examples +Check out the [examples](/examples) to see how easy +it is to create your own server. diff --git a/templates/server/api.mustache b/templates/server/api.mustache new file mode 100644 index 00000000..85a4e34f --- /dev/null +++ b/templates/server/api.mustache @@ -0,0 +1,25 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "net/http" + + "github.com/coinbase/rosetta-sdk-go/models" +) + +{{#apiInfo}}{{#apis}} +// {{classname}}Router defines the required methods for binding the api requests to a responses for the {{classname}} +// The {{classname}}Router implementation should parse necessary information from the http request, +// pass the data to a {{classname}}Servicer to perform the required actions, then write the service results to the http response. +type {{classname}}Router interface { {{#operations}}{{#operation}} + {{operationId}}(http.ResponseWriter, *http.Request){{/operation}}{{/operations}} +}{{/apis}}{{/apiInfo}}{{#apiInfo}}{{#apis}} + + +// {{classname}}Servicer defines the api actions for the {{classname}} service +// This interface intended to stay up to date with the openapi yaml used to generate it, +// while the service implementation can ignored with the .openapi-generator-ignore file +// and updated with the logic required for the API. +type {{classname}}Servicer interface { {{#operations}}{{#operation}} + {{operationId}}({{#allParams}}*models.{{dataType}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) (*models.{{returnType}}, *models.Error){{/operation}}{{/operations}} +}{{/apis}}{{/apiInfo}} diff --git a/templates/server/controller-api.mustache b/templates/server/controller-api.mustache new file mode 100644 index 00000000..ba094750 --- /dev/null +++ b/templates/server/controller-api.mustache @@ -0,0 +1,77 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/coinbase/rosetta-sdk-go/models" + "github.com/coinbase/rosetta-sdk-go/asserter" +) + +// A {{classname}}Controller binds http requests to an api service and writes the service results to the http response +type {{classname}}Controller struct { + service {{classname}}Servicer +} + +// New{{classname}}Controller creates a default api controller +func New{{classname}}Controller(s {{classname}}Servicer) Router { + return &{{classname}}Controller{ service: s } +} + +// Routes returns all of the api route for the {{classname}}Controller +func (c *{{classname}}Controller) Routes() Routes { + return Routes{ {{#operations}}{{#operation}} + { + "{{operationId}}", + strings.ToUpper("{{httpMethod}}"), + "{{{basePathWithoutHost}}}{{{path}}}", + c.{{operationId}}, + },{{/operation}}{{/operations}} + } +}{{#operations}}{{#operation}} + +// {{nickname}} - {{{summary}}} +func (c *{{classname}}Controller) {{nickname}}(w http.ResponseWriter, r *http.Request) { {{#allParams}}{{#isHeaderParam}} + {{paramName}} := r.Header.Get("{{paramName}}"){{/isHeaderParam}}{{#isBodyParam}} + {{paramName}} := &models.{{dataType}}{} + if err := json.NewDecoder(r.Body).Decode(&{{paramName}}); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + // Assert that {{dataType}} is correct + if err := asserter.{{dataType}}({{paramName}}); err != nil { + err = EncodeJSONResponse(&models.Error{ + Message: err.Error(), + }, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + {{/isBodyParam}}{{/allParams}} + result, serviceErr := c.service.{{nickname}}({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) + if serviceErr != nil { + err := EncodeJSONResponse(serviceErr, http.StatusInternalServerError, w) + if err != nil { + log.Fatal(err) + } + + return + } + + if err := EncodeJSONResponse(result, http.StatusOK, w); err != nil { + log.Fatal(err) + } +}{{/operation}}{{/operations}} diff --git a/templates/server/logger.mustache b/templates/server/logger.mustache new file mode 100644 index 00000000..e8464d6a --- /dev/null +++ b/templates/server/logger.mustache @@ -0,0 +1,24 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "log" + "net/http" + "time" +) + +func Logger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s %s %s %s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} diff --git a/templates/server/partial_header.mustache b/templates/server/partial_header.mustache new file mode 100644 index 00000000..a0a1639a --- /dev/null +++ b/templates/server/partial_header.mustache @@ -0,0 +1 @@ +// Generated by: OpenAPI Generator (https://openapi-generator.tech) diff --git a/templates/server/routers.mustache b/templates/server/routers.mustache new file mode 100644 index 00000000..39f0aedf --- /dev/null +++ b/templates/server/routers.mustache @@ -0,0 +1,69 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +// A Route defines the parameters for an api endpoint +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// Routes are a collection of defined api endpoints +type Routes []Route + +// Router defines the required methods for retrieving api routes +type Router interface { + Routes() Routes +} + +// CorsMiddleware handle CORS and ensures OPTIONS requests are +// handled properly. +func CorsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST,OPTIONS") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +// NewRouter creates a new router for any number of api routers +func NewRouter(routers ...Router) http.Handler { + router := mux.NewRouter().StrictSlash(true) + for _, api := range routers { + for _, route := range api.Routes() { + var handler http.Handler + handler = route.HandlerFunc + handler = Logger(handler, route.Name) + + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(handler) + } + } + + // use custom middleware because could not get mux handler to work in Chrome + return CorsMiddleware(router) +} + +// EncodeJSONResponse uses the json encoder to write an interface to the http response with an optional status code +func EncodeJSONResponse(i interface{}, status int, w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(status) + + return json.NewEncoder(w).Encode(i) +}