diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..1cb5a299 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,26 @@ +# 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. + +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.13 + working_directory: /go/src/github.com/coinbase/rosetta-sdk-go + steps: + - checkout + - run: make deps + - run: make test + - run: make lint + - run: make check-license diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..4e098f8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,19 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..36014cde --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e4f01e9a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +Fixes # . + +### Motivation + + +### Solution + + +### Open questions + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6917184c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to Rosetta-SDK-Go + +## Code of Conduct + +All interactions with this project follow our [Code of Conduct][code-of-conduct]. +By participating, you are expected to honor this code. Violators can be banned +from further participation in this project, or potentially all Coinbase projects. + +[code-of-conduct]: https://github.com/coinbase/code-of-conduct + +## Bug Reports + +* Ensure your issue [has not already been reported][1]. It may already be fixed! +* Include the steps you carried out to produce the problem. +* Include the behavior you observed along with the behavior you expected, and + why you expected it. +* Include any relevant stack traces or debugging output. + +## Feature Requests + +We welcome feedback with or without pull requests. If you have an idea for how +to improve the project, great! All we ask is that you take the time to write a +clear and concise explanation of what need you are trying to solve. If you have +thoughts on _how_ it can be solved, include those too! + +The best way to see a feature added, however, is to submit a pull request. + +## Pull Requests + +* Before creating your pull request, it's usually worth asking if the code + you're planning on writing will actually be considered for merging. You can + do this by [opening an issue][1] and asking. It may also help give the + maintainers context for when the time comes to review your code. + +* Ensure your [commit messages are well-written][2]. This can double as your + pull request message, so it pays to take the time to write a clear message. + +* Add tests for your feature. You should be able to look at other tests for + examples. If you're unsure, don't hesitate to [open an issue][1] and ask! + +* Submit your pull request! + +## Support Requests + +For security reasons, any communication referencing support tickets for Coinbase +products will be ignored. The request will have its content redacted and will +be locked to prevent further discussion. + +All support requests must be made via [our support team][3]. + +[1]: https://github.com/coinbase/rosetta-sdk-go/issues +[2]: https://medium.com/brigade-engineering/the-secrets-to-great-commit-messages-106fc0a92a25 +[3]: https://support.coinbase.com/customer/en/portal/articles/2288496-how-can-i-contact-coinbase-support- diff --git a/LICENSE b/LICENSE.txt similarity index 99% rename from LICENSE rename to LICENSE.txt index 261eeb9e..5df8419c 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..33c13776 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: deps gen lint test add-license check-license circleci-local shellcheck salus +LICENCE_SCRIPT=addlicense -c "Coinbase, Inc." -l "apache" -v + +deps: + go get ./... + go get github.com/stretchr/testify + go get github.com/davecgh/go-spew + go get golang.org/x/lint/golint + go get github.com/google/addlicense + +gen: + ./codegen.sh + +lint: + golint -set_exit_status ./asserter/... ./fetcher/... ./gen/... + +test: + go test -v ./asserter ./fetcher + +add-license: + ${LICENCE_SCRIPT} . + +check-license: + ${LICENCE_SCRIPT} -check . + +circleci-local: + circleci local execute + +shellcheck: + shellcheck codegen.sh + +salus: + docker run --rm -t -v ${PWD}:/home/repo coinbase/salus diff --git a/README.md b/README.md new file mode 100644 index 00000000..acc14907 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# rosetta-sdk-go + +[![Coinbase](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master.svg?style=svg)](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master) + +## 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: 1.2.4 +- Package version: 0.0.1 + +## 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) + +## 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/asserter/account.go b/asserter/account.go new file mode 100644 index 00000000..960cd411 --- /dev/null +++ b/asserter/account.go @@ -0,0 +1,110 @@ +// 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 ( + "fmt" + "reflect" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// 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 +// struct (including the SubAccountIdentifier). +func containsAccountIdentifier( + identifiers []*rosetta.AccountIdentifier, + identifier *rosetta.AccountIdentifier, +) bool { + for _, ident := range identifiers { + if reflect.DeepEqual(ident, identifier) { + return true + } + } + + return false +} + +// 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 +// struct (including currency.Metadata). +func containsCurrency(currencies []*rosetta.Currency, currency *rosetta.Currency) bool { + for _, curr := range currencies { + if reflect.DeepEqual(curr, currency) { + return true + } + } + + return false +} + +// assertBalanceAmounts returns an error if a slice +// of rosetta.Amount returned as an rosetta.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) + for _, amount := range amounts { + // Ensure a currency is used at most once in balance.Amounts + if containsCurrency(currencies, amount.Currency) { + return fmt.Errorf("currency %+v used in balance multiple times", amount.Currency) + } + currencies = append(currencies, amount.Currency) + + if err := Amount(amount); err != nil { + return err + } + } + + return nil +} + +// 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. +func AccountBalance( + block *rosetta.BlockIdentifier, + balances []*rosetta.Balance, +) error { + if err := BlockIdentifier(block); err != nil { + return err + } + + accounts := make([]*rosetta.AccountIdentifier, 0) + for _, balance := range balances { + if err := AccountIdentifier(balance.AccountIdentifier); err != nil { + return err + } + + // Ensure an account identifier is used at most once in a balance response + if containsAccountIdentifier(accounts, balance.AccountIdentifier) { + return fmt.Errorf("account identifier %+v used in balance multiple times", balance.AccountIdentifier) + } + accounts = append(accounts, balance.AccountIdentifier) + + if err := assertBalanceAmounts(balance.Amounts); err != nil { + return err + } + } + + return nil +} diff --git a/asserter/account_test.go b/asserter/account_test.go new file mode 100644 index 00000000..050cb9a6 --- /dev/null +++ b/asserter/account_test.go @@ -0,0 +1,349 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asserter + +import ( + "errors" + "fmt" + "testing" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" + + "github.com/stretchr/testify/assert" +) + +func TestContainsCurrency(t *testing.T) { + var tests = map[string]struct { + currencies []*rosetta.Currency + currency *rosetta.Currency + contains bool + }{ + "simple contains": { + currencies: []*rosetta.Currency{ + &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + }, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + contains: true, + }, + "complex contains": { + currencies: []*rosetta.Currency{ + &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + contains: true, + }, + "empty": { + currencies: []*rosetta.Currency{}, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + contains: false, + }, + "symbol mismatch": { + currencies: []*rosetta.Currency{ + &rosetta.Currency{ + Symbol: "ERX", + Decimals: 8, + }, + }, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 6, + }, + contains: false, + }, + "decimal mismatch": { + currencies: []*rosetta.Currency{ + &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + }, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 6, + }, + contains: false, + }, + "metadata mismatch": { + currencies: []*rosetta.Currency{ + &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + Metadata: &map[string]interface{}{ + "blah": "bye", + }, + }, + contains: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + exists := containsCurrency(test.currencies, test.currency) + assert.Equal(t, test.contains, exists) + }) + } +} + +func TestContainsAccountIdentifier(t *testing.T) { + var tests = map[string]struct { + identifiers []*rosetta.AccountIdentifier + identifier *rosetta.AccountIdentifier + contains bool + }{ + "simple contains": { + identifiers: []*rosetta.AccountIdentifier{ + &rosetta.AccountIdentifier{ + Address: "acct1", + }, + }, + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + }, + contains: true, + }, + "complex contains": { + identifiers: []*rosetta.AccountIdentifier{ + &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct1", + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + }, + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct1", + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + contains: true, + }, + "simple mismatch": { + identifiers: []*rosetta.AccountIdentifier{ + &rosetta.AccountIdentifier{ + Address: "acct1", + }, + }, + identifier: &rosetta.AccountIdentifier{ + Address: "acct2", + }, + contains: false, + }, + "empty": { + identifiers: []*rosetta.AccountIdentifier{}, + identifier: &rosetta.AccountIdentifier{ + Address: "acct2", + }, + contains: false, + }, + "subaccount mismatch": { + identifiers: []*rosetta.AccountIdentifier{ + &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct2", + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + }, + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct1", + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + contains: false, + }, + "metadata mismatch": { + identifiers: []*rosetta.AccountIdentifier{ + &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct1", + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + }, + }, + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "subacct1", + Metadata: &map[string]interface{}{ + "blah": "bye", + }, + }, + }, + contains: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + exists := containsAccountIdentifier(test.identifiers, test.identifier) + assert.Equal(t, test.contains, exists) + }) + } +} + +func TestAccoutBalance(t *testing.T) { + validBlock := &rosetta.BlockIdentifier{ + Index: 1000, + Hash: "jsakdl", + } + + invalidBlock := &rosetta.BlockIdentifier{ + Index: 1, + Hash: "", + } + + validIdentifier := &rosetta.AccountIdentifier{ + Address: "acct1", + } + + invalidIdentifier := &rosetta.AccountIdentifier{ + Address: "", + } + + validAmount := &rosetta.Amount{ + Value: "100", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + + var tests = map[string]struct { + block *rosetta.BlockIdentifier + balances []*rosetta.Balance + err error + }{ + "simple balance": { + block: validBlock, + balances: []*rosetta.Balance{ + &rosetta.Balance{ + AccountIdentifier: validIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + }, + }, + }, + err: nil, + }, + "invalid block": { + block: invalidBlock, + balances: []*rosetta.Balance{ + &rosetta.Balance{ + AccountIdentifier: validIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + }, + }, + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + "invalid account identifier": { + block: validBlock, + balances: []*rosetta.Balance{ + &rosetta.Balance{ + AccountIdentifier: invalidIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + }, + }, + }, + err: errors.New("Account.Address is missing"), + }, + "duplicate currency": { + block: validBlock, + balances: []*rosetta.Balance{ + &rosetta.Balance{ + AccountIdentifier: validIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + validAmount, + }, + }, + }, + err: fmt.Errorf("currency %+v used in balance multiple times", validAmount.Currency), + }, + "duplicate identifier": { + block: validBlock, + balances: []*rosetta.Balance{ + &rosetta.Balance{ + AccountIdentifier: validIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + }, + }, + &rosetta.Balance{ + AccountIdentifier: validIdentifier, + Amounts: []*rosetta.Amount{ + validAmount, + }, + }, + }, + err: fmt.Errorf("account identifier %+v used in balance multiple times", validIdentifier), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := AccountBalance(test.block, test.balances) + assert.Equal(t, test.err, err) + }) + } +} diff --git a/asserter/asserter.go b/asserter/asserter.go new file mode 100644 index 00000000..8e511175 --- /dev/null +++ b/asserter/asserter.go @@ -0,0 +1,71 @@ +// 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 ( + "context" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// Asserter contains all logic to perform static +// validation on Rosetta Server responses. +type Asserter struct { + operationTypes []string + operationStatusMap map[string]bool + submissionStatusMap map[string]bool + genesisIndex int64 +} + +// New constructs a new Asserter. +func New( + ctx context.Context, + networkResponse *rosetta.NetworkStatusResponse, +) *Asserter { + asserter := &Asserter{ + operationTypes: networkResponse.Options.OperationTypes, + genesisIndex: networkResponse.NetworkStatus.NetworkInformation.GenesisBlockIdentifier.Index, + } + + asserter.operationStatusMap = map[string]bool{} + for _, status := range networkResponse.Options.OperationStatuses { + asserter.operationStatusMap[status.Status] = status.Successful + } + + asserter.submissionStatusMap = map[string]bool{} + for _, status := range networkResponse.Options.SubmissionStatuses { + asserter.submissionStatusMap[status.Status] = status.Successful + } + + return asserter +} + +func (a *Asserter) operationStatuses() []string { + statuses := []string{} + for k := range a.operationStatusMap { + statuses = append(statuses, k) + } + + return statuses +} + +func (a *Asserter) submissionStatuses() []string { + statuses := []string{} + for k := range a.submissionStatusMap { + statuses = append(statuses, k) + } + + return statuses +} diff --git a/asserter/block.go b/asserter/block.go new file mode 100644 index 00000000..c23f9edc --- /dev/null +++ b/asserter/block.go @@ -0,0 +1,249 @@ +// 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 ( + "context" + "errors" + "fmt" + "math/big" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// Amount ensures a rosetta.Amount has an +// integer value, specified precision, and symbol. +func Amount(amount *rosetta.Amount) error { + if amount == nil || amount.Value == "" { + return errors.New("Amount.Value is missing") + } + + _, ok := new(big.Int).SetString(amount.Value, 10) + if !ok { + return fmt.Errorf("Amount.Value not an integer %s", amount.Value) + } + + if amount.Currency == nil { + return errors.New("Amount.Currency is nil") + } + + if amount.Currency.Symbol == "" { + return errors.New("Amount.Currency.Symbol is empty") + } + + if amount.Currency.Decimals <= 0 { + return errors.New("Amount.Currency.Decimals must be > 0") + } + + return nil +} + +// contains checks if a string is contained in a slice +// of strings. +func contains(valid []string, value string) bool { + for _, v := range valid { + if v == value { + return true + } + } + + return false +} + +// OperationIdentifier returns an error if index of the +// rosetta.Operation is out-of-order or if the NetworkIndex is +// invalid. +func OperationIdentifier( + identifier *rosetta.OperationIdentifier, + index int64, +) error { + if identifier == nil || identifier.Index != index { + return errors.New("Operation.OperationIdentifier.Index invalid") + } + + if identifier.NetworkIndex != nil && *identifier.NetworkIndex < 0 { + return errors.New("Operation.OperationIdentifier.NetworkIndex invalid") + } + + return nil +} + +// AccountIdentifier returns an error if a rosetta.AccountIdentifier +// is missing an address or a provided SubAccount is missing an identifier. +func AccountIdentifier(account *rosetta.AccountIdentifier) error { + if account == nil { + return errors.New("Account is nil") + } + + if account.Address == "" { + return errors.New("Account.Address is missing") + } + + if account.SubAccount == nil { + return nil + } + + if account.SubAccount.SubAccount == "" { + return errors.New("Account.SubAccount.SubAccount is missing") + } + + return nil +} + +// OperationSuccessful returns a boolean indicating if a rosetta.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) { + val, ok := a.operationStatusMap[operation.Status] + if !ok { + return false, fmt.Errorf("%s not found", operation.Status) + } + + return val, nil +} + +// Operation ensures a rosetta.Operation has a valid +// type, status, and amount. +func (a *Asserter) Operation( + operation *rosetta.Operation, + index int64, +) error { + if operation == nil { + return errors.New("Operation is nil") + } + + if err := OperationIdentifier(operation.OperationIdentifier, index); err != nil { + return err + } + + if operation.Type == "" || !contains(a.operationTypes, operation.Type) { + return fmt.Errorf("Operation.Type %s is invalid", operation.Type) + } + + if operation.Status == "" || !contains(a.operationStatuses(), operation.Status) { + return fmt.Errorf("Operation.Status %s is invalid", operation.Status) + } + + if operation.Amount == nil { + return nil + } + + if err := AccountIdentifier(operation.Account); err != nil { + return err + } + + return Amount(operation.Amount) +} + +// BlockIdentifier ensures a rosetta.BlockIdentifier +// is well-formatted. +func BlockIdentifier(blockIdentifier *rosetta.BlockIdentifier) error { + if blockIdentifier == nil || blockIdentifier.Hash == "" { + return errors.New("BlockIdentifier.Hash is missing") + } + + if blockIdentifier.Index < 0 { + return errors.New("BlockIdentifier.Index is negative") + } + + return nil +} + +// TransactionIdentifier returns an error if a +// rosetta.TransactionIdentifier has an invalid hash. +func TransactionIdentifier( + transactionIdentifier *rosetta.TransactionIdentifier, +) error { + if transactionIdentifier == nil || transactionIdentifier.Hash == "" { + return errors.New("Transaction.TransactionIdentifier.Hash is missing") + } + + return nil +} + +// Transaction returns an error if the rosetta.TransactionIdentifier +// is invalid, if any rosetta.Operation within the rosetta.Transaction +// is invalid, or if any operation index is reused within a transaction. +func (a *Asserter) Transaction( + transaction *rosetta.Transaction, +) error { + if transaction == nil { + return errors.New("transaction is nil") + } + + if err := TransactionIdentifier(transaction.TransactionIdentifier); err != nil { + return err + } + + for i, op := range transaction.Operations { + if err := a.Operation(op, int64(i)); err != nil { + return err + } + } + + return nil +} + +// Timestamp returns an error if the timestamp +// on a block is less than or equal to 0. +func Timestamp(timestamp int64) error { + if timestamp <= 0 { + return fmt.Errorf("Timestamp is invalid %d", timestamp) + } + + return nil +} + +// Block runs a basic set of assertions for each returned block. +func (a *Asserter) Block( + ctx context.Context, + block *rosetta.Block, +) error { + if block == nil { + return errors.New("block is nil") + } + + if err := BlockIdentifier(block.BlockIdentifier); err != nil { + return err + } + + if err := BlockIdentifier(block.ParentBlockIdentifier); err != nil { + return err + } + + // Only apply some assertions if the block index is not the + // genesis index. + if a.genesisIndex != block.BlockIdentifier.Index { + if block.BlockIdentifier.Hash == block.ParentBlockIdentifier.Hash { + return errors.New("Block.BlockIdentifier.Hash == Block.ParentBlockIdentifier.Hash") + } + + if block.BlockIdentifier.Index <= block.ParentBlockIdentifier.Index { + return errors.New("Block.BlockIdentifier.Index <= Block.ParentBlockIdentifier.Index") + } + } + + if err := Timestamp(block.Timestamp); err != nil { + return err + } + + for _, transaction := range block.Transactions { + if err := a.Transaction(transaction); err != nil { + return err + } + } + + return nil +} diff --git a/asserter/block_test.go b/asserter/block_test.go new file mode 100644 index 00000000..4f1cc97b --- /dev/null +++ b/asserter/block_test.go @@ -0,0 +1,522 @@ +// 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 ( + "context" + "errors" + "testing" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" + + "github.com/stretchr/testify/assert" +) + +func TestAmount(t *testing.T) { + var tests = map[string]struct { + amount *rosetta.Amount + err error + }{ + "valid amount": { + amount: &rosetta.Amount{ + Value: "100000", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 1, + }, + }, + err: nil, + }, + "valid negative amount": { + amount: &rosetta.Amount{ + Value: "-100000", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 1, + }, + }, + err: nil, + }, + "nil amount": { + amount: nil, + err: errors.New("Amount.Value is missing"), + }, + "nil currency": { + amount: &rosetta.Amount{ + Value: "100000", + }, + err: errors.New("Amount.Currency is nil"), + }, + "invalid non-number": { + amount: &rosetta.Amount{ + Value: "blah", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 1, + }, + }, + err: errors.New("Amount.Value not an integer blah"), + }, + "invalid integer format": { + amount: &rosetta.Amount{ + Value: "1.0", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 1, + }, + }, + err: errors.New("Amount.Value not an integer 1.0"), + }, + "invalid non-integer": { + amount: &rosetta.Amount{ + Value: "1.1", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 1, + }, + }, + err: errors.New("Amount.Value not an integer 1.1"), + }, + "invalid symbol": { + amount: &rosetta.Amount{ + Value: "11", + Currency: &rosetta.Currency{ + Decimals: 1, + }, + }, + err: errors.New("Amount.Currency.Symbol is empty"), + }, + "invalid decimals": { + amount: &rosetta.Amount{ + Value: "111", + Currency: &rosetta.Currency{ + Symbol: "BTC", + }, + }, + err: errors.New("Amount.Currency.Decimals must be > 0"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := Amount(test.amount) + assert.Equal(t, test.err, err) + }) + } +} + +func TestOperationIdentifier(t *testing.T) { + var ( + validNetworkIndex = int64(1) + invalidNetworkIndex = int64(-1) + ) + + var tests = map[string]struct { + identifier *rosetta.OperationIdentifier + index int64 + err error + }{ + "valid identifier": { + identifier: &rosetta.OperationIdentifier{ + Index: 0, + }, + index: 0, + err: nil, + }, + "nil identifier": { + identifier: nil, + index: 0, + err: errors.New("Operation.OperationIdentifier.Index invalid"), + }, + "out-of-order index": { + identifier: &rosetta.OperationIdentifier{ + Index: 0, + }, + index: 1, + err: errors.New("Operation.OperationIdentifier.Index invalid"), + }, + "valid identifer with network index": { + identifier: &rosetta.OperationIdentifier{ + Index: 0, + NetworkIndex: &validNetworkIndex, + }, + index: 0, + err: nil, + }, + "invalid identifer with network index": { + identifier: &rosetta.OperationIdentifier{ + Index: 0, + NetworkIndex: &invalidNetworkIndex, + }, + index: 0, + err: errors.New("Operation.OperationIdentifier.NetworkIndex invalid"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := OperationIdentifier(test.identifier, test.index) + assert.Equal(t, test.err, err) + }) + } +} + +func TestAccountIdentifier(t *testing.T) { + var tests = map[string]struct { + identifier *rosetta.AccountIdentifier + err error + }{ + "valid identifier": { + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + }, + err: nil, + }, + "invalid address": { + identifier: &rosetta.AccountIdentifier{ + Address: "", + }, + err: errors.New("Account.Address is missing"), + }, + "valid identifer with subaccount": { + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "acct2", + }, + }, + err: nil, + }, + "invalid identifer with subaccount": { + identifier: &rosetta.AccountIdentifier{ + Address: "acct1", + SubAccount: &rosetta.SubAccountIdentifier{ + SubAccount: "", + }, + }, + err: errors.New("Account.SubAccount.SubAccount is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := AccountIdentifier(test.identifier) + assert.Equal(t, test.err, err) + }) + } +} + +func TestOperation(t *testing.T) { + var ( + validAmount = &rosetta.Amount{ + Value: "1000", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + + validAccount = &rosetta.AccountIdentifier{ + Address: "test", + } + ) + + var tests = map[string]struct { + operation *rosetta.Operation + index int64 + successful bool + err error + }{ + "valid operation": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: validAccount, + Amount: validAmount, + }, + index: int64(1), + successful: true, + err: nil, + }, + "valid operation no account": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "SUCCESS", + }, + index: int64(1), + successful: true, + err: nil, + }, + "nil operation": { + operation: nil, + index: int64(1), + err: errors.New("Operation is nil"), + }, + "invalid operation no account": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Amount: validAmount, + }, + index: int64(1), + err: errors.New("Account is nil"), + }, + "invalid operation empty account": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "SUCCESS", + Account: &rosetta.AccountIdentifier{}, + Amount: validAmount, + }, + index: int64(1), + err: errors.New("Account.Address is missing"), + }, + "invalid operation invalid index": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "SUCCESS", + }, + index: int64(2), + err: errors.New("Operation.OperationIdentifier.Index invalid"), + }, + "invalid operation invalid type": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "STAKE", + Status: "SUCCESS", + }, + index: int64(1), + err: errors.New("Operation.Type STAKE is invalid"), + }, + "unsuccessful operation": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "FAILURE", + }, + index: int64(1), + successful: false, + err: nil, + }, + "invalid operation invalid status": { + operation: &rosetta.Operation{ + OperationIdentifier: &rosetta.OperationIdentifier{ + Index: int64(1), + }, + Type: "PAYMENT", + Status: "DEFERRED", + }, + index: int64(1), + err: errors.New("Operation.Status DEFERRED is invalid"), + }, + } + + for name, test := range tests { + asserter := New( + context.Background(), + &rosetta.NetworkStatusResponse{ + NetworkStatus: &rosetta.NetworkStatus{ + NetworkInformation: &rosetta.NetworkInformation{ + GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + Index: 0, + }, + }, + }, + Options: &rosetta.Options{ + OperationStatuses: []*rosetta.OperationStatus{ + &rosetta.OperationStatus{ + Status: "SUCCESS", + Successful: true, + }, + &rosetta.OperationStatus{ + Status: "FAILURE", + Successful: false, + }, + }, + OperationTypes: []string{ + "PAYMENT", + }, + }, + }, + ) + t.Run(name, func(t *testing.T) { + err := asserter.Operation(test.operation, test.index) + assert.Equal(t, test.err, err) + if err == nil { + successful, err := asserter.OperationSuccessful(test.operation) + assert.NoError(t, err) + assert.Equal(t, test.successful, successful) + } + }) + } +} + +func TestBlock(t *testing.T) { + validBlockIdentifier := &rosetta.BlockIdentifier{ + Hash: "blah", + Index: 100, + } + validParentBlockIdentifier := &rosetta.BlockIdentifier{ + Hash: "blah parent", + Index: 99, + } + validTransaction := &rosetta.Transaction{ + TransactionIdentifier: &rosetta.TransactionIdentifier{ + Hash: "blah", + }, + } + var tests = map[string]struct { + block *rosetta.Block + genesisIndex int64 + err error + }{ + "valid block": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: nil, + }, + "genesis block": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validBlockIdentifier, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + genesisIndex: validBlockIdentifier.Index, + err: nil, + }, + "nil block": { + block: nil, + err: errors.New("block is nil"), + }, + "nil block hash": { + block: &rosetta.Block{ + BlockIdentifier: nil, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + "invalid block hash": { + block: &rosetta.Block{ + BlockIdentifier: &rosetta.BlockIdentifier{}, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + "block previous hash missing": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: &rosetta.BlockIdentifier{}, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("BlockIdentifier.Hash is missing"), + }, + "invalid parent block index": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: &rosetta.BlockIdentifier{ + Hash: validParentBlockIdentifier.Hash, + Index: validBlockIdentifier.Index, + }, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("Block.BlockIdentifier.Index <= Block.ParentBlockIdentifier.Index"), + }, + "invalid parent block hash": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: &rosetta.BlockIdentifier{ + Hash: validBlockIdentifier.Hash, + Index: validParentBlockIdentifier.Index, + }, + Timestamp: 1, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("Block.BlockIdentifier.Hash == Block.ParentBlockIdentifier.Hash"), + }, + "invalid block timestamp": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Transactions: []*rosetta.Transaction{validTransaction}, + }, + err: errors.New("Timestamp is invalid 0"), + }, + "invalid block transaction": { + block: &rosetta.Block{ + BlockIdentifier: validBlockIdentifier, + ParentBlockIdentifier: validParentBlockIdentifier, + Timestamp: 1, + Transactions: []*rosetta.Transaction{ + &rosetta.Transaction{}, + }, + }, + err: errors.New("Transaction.TransactionIdentifier.Hash is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + asserter := New( + context.Background(), + &rosetta.NetworkStatusResponse{ + NetworkStatus: &rosetta.NetworkStatus{ + NetworkInformation: &rosetta.NetworkInformation{ + GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + Index: test.genesisIndex, + }, + }, + }, + Options: &rosetta.Options{ + SubmissionStatuses: []*rosetta.SubmissionStatus{}, + OperationStatuses: []*rosetta.OperationStatus{}, + OperationTypes: []string{}, + }, + }, + ) + err := asserter.Block(context.Background(), test.block) + assert.Equal(t, test.err, err) + }) + } +} diff --git a/asserter/construction.go b/asserter/construction.go new file mode 100644 index 00000000..fd0ead15 --- /dev/null +++ b/asserter/construction.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 asserter + +import ( + "errors" + "fmt" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// TransactionConstruction returns an error if +// the SuggestedFee is not a valid rosetta.Amount. +func TransactionConstruction( + response *rosetta.TransactionConstructionResponse, +) error { + return Amount(response.SuggestedFee) +} + +// TransactionSubmit returns an error if +// the rosetta.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, +) error { + if err := TransactionIdentifier(response.TransactionIdentifier); err != nil { + return err + } + + if response.Status == "" { + return errors.New("Submission.Status is missing") + } + + if !contains(a.submissionStatuses(), response.Status) { + return fmt.Errorf("Submission.Status %s is invalid", response.Status) + } + + return nil +} diff --git a/asserter/construction_test.go b/asserter/construction_test.go new file mode 100644 index 00000000..03d1aefe --- /dev/null +++ b/asserter/construction_test.go @@ -0,0 +1,163 @@ +// 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 ( + "context" + "errors" + "fmt" + "testing" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" + + "github.com/stretchr/testify/assert" +) + +func TestTransactionConstruction(t *testing.T) { + validAmount := &rosetta.Amount{ + Value: "1", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + + invalidAmount := &rosetta.Amount{ + Value: "", + Currency: &rosetta.Currency{ + Symbol: "BTC", + Decimals: 8, + }, + } + + var tests = map[string]struct { + response *rosetta.TransactionConstructionResponse + err error + }{ + "valid response": { + response: &rosetta.TransactionConstructionResponse{ + SuggestedFee: validAmount, + }, + err: nil, + }, + "valid response with metadata": { + response: &rosetta.TransactionConstructionResponse{ + SuggestedFee: validAmount, + Metadata: &map[string]interface{}{ + "blah": "hello", + }, + }, + err: nil, + }, + "invalid amount": { + response: &rosetta.TransactionConstructionResponse{ + SuggestedFee: invalidAmount, + }, + err: errors.New("Amount.Value is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := TransactionConstruction(test.response) + assert.Equal(t, test.err, err) + }) + } +} + +func TestTransactionSubmit(t *testing.T) { + validStatuses := []*rosetta.SubmissionStatus{ + &rosetta.SubmissionStatus{ + Status: "fail", + Successful: false, + }, + &rosetta.SubmissionStatus{ + Status: "mempool", + Successful: true, + }, + } + var tests = map[string]struct { + response *rosetta.TransactionSubmitResponse + err error + }{ + "valid response": { + response: &rosetta.TransactionSubmitResponse{ + TransactionIdentifier: &rosetta.TransactionIdentifier{ + Hash: "tx1", + }, + Status: "mempool", + }, + err: nil, + }, + "invalid transaction identifier": { + response: &rosetta.TransactionSubmitResponse{ + Status: "mempool", + }, + err: errors.New("Transaction.TransactionIdentifier.Hash is missing"), + }, + "invalid status": { + response: &rosetta.TransactionSubmitResponse{ + TransactionIdentifier: &rosetta.TransactionIdentifier{ + Hash: "tx1", + }, + Status: "maybe", + }, + err: fmt.Errorf("Submission.Status %s is invalid", "maybe"), + }, + "empty status": { + response: &rosetta.TransactionSubmitResponse{ + TransactionIdentifier: &rosetta.TransactionIdentifier{ + Hash: "tx1", + }, + }, + err: errors.New("Submission.Status is missing"), + }, + } + + for name, test := range tests { + asserter := New( + context.Background(), + &rosetta.NetworkStatusResponse{ + NetworkStatus: &rosetta.NetworkStatus{ + NetworkInformation: &rosetta.NetworkInformation{ + GenesisBlockIdentifier: &rosetta.BlockIdentifier{ + Index: 0, + }, + }, + }, + Options: &rosetta.Options{ + OperationStatuses: []*rosetta.OperationStatus{ + &rosetta.OperationStatus{ + Status: "SUCCESS", + Successful: true, + }, + &rosetta.OperationStatus{ + Status: "FAILURE", + Successful: false, + }, + }, + OperationTypes: []string{ + "PAYMENT", + }, + SubmissionStatuses: validStatuses, + }, + }, + ) + t.Run(name, func(t *testing.T) { + err := asserter.TransactionSubmit(test.response) + assert.Equal(t, test.err, err) + }) + } +} diff --git a/asserter/mempool.go b/asserter/mempool.go new file mode 100644 index 00000000..0abafa77 --- /dev/null +++ b/asserter/mempool.go @@ -0,0 +1,35 @@ +// 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 ( + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// MempoolTransactions returns an error if any +// rosetta.TransactionIdentifier returns is missing a hash. +// The correctness of each populated MempoolTransaction is +// asserted by Transaction. +func MempoolTransactions( + transactions []*rosetta.TransactionIdentifier, +) error { + for _, t := range transactions { + if err := TransactionIdentifier(t); err != nil { + return err + } + } + + return nil +} diff --git a/asserter/network.go b/asserter/network.go new file mode 100644 index 00000000..b3b85398 --- /dev/null +++ b/asserter/network.go @@ -0,0 +1,298 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asserter + +import ( + "errors" + "fmt" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// SubNetworkIdentifier asserts a rosetta.SubNetworkIdentifer is valid (if not nil). +func SubNetworkIdentifier(subNetworkIdentifier *rosetta.SubNetworkIdentifier) error { + if subNetworkIdentifier == nil { + return nil + } + + if subNetworkIdentifier.SubNetwork == "" { + return errors.New("NetworkIdentifier.SubNetworkIdentifier.SubNetwork is missing") + } + + return nil +} + +// NetworkIdentifier ensures a rosetta.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") + } + + if network.Network == "" { + return errors.New("NetworkIdentifier.Network is missing") + } + + return SubNetworkIdentifier(network.SubNetworkIdentifier) +} + +// PartialNetworkIdentifier ensures a rosetta.PartialNetworkIdentifier has +// a valid blockchain and network. +func PartialNetworkIdentifier(identifier *rosetta.PartialNetworkIdentifier) error { + if identifier == nil || identifier.Blockchain == "" { + return errors.New("PartialNetworkIdentifier.Blockchain is missing") + } + + if identifier.Network == "" { + return errors.New("PartialNetworkIdentifier.Network is missing") + } + + return nil +} + +// Peer ensures a rosetta.Peer has a valid peer_id. +func Peer(peer *rosetta.Peer) error { + if peer == nil || peer.PeerID == "" { + return errors.New("Peer.PeerID is missing") + } + + return nil +} + +// SubNetworkStatus ensures a rosetta.SubNetworkStatus is valid. +func SubNetworkStatus(subNetwork *rosetta.SubNetworkStatus) error { + if err := SubNetworkIdentifier(subNetwork.SubNetworkIdentifier); err != nil { + return err + } + + if err := NetworkInformation(subNetwork.NetworkInformation); err != nil { + return err + } + + return nil +} + +// Version ensures the version of the node is +// returned. +func Version(version *rosetta.Version) error { + if version == nil { + return errors.New("version is nil") + } + + // Assert RosettaVersion against what client can assert + // against. This could be multiple versions. + if version.RosettaVersion != rosetta.APIVersion { + return fmt.Errorf("Version.RosettaVersion %s is invalid", version.RosettaVersion) + } + + if version.NodeVersion == "" { + return errors.New("Version.NodeVersion is missing") + } + + if version.MiddlewareVersion != nil && *version.MiddlewareVersion == "" { + return errors.New("Version.MiddlewareVersion is missing") + } + + return nil +} + +// StringArray ensures all strings in an array +// are non-empty strings. +func StringArray(arrName string, arr []string) error { + if len(arr) == 0 { + return fmt.Errorf("no %s found", arrName) + } + + for _, s := range arr { + if s == "" { + return fmt.Errorf("%s has an empty string", arrName) + } + } + + return nil +} + +// SupportedMethods ensures any methods +// returned by the Rosetta Interface server are valid. +func SupportedMethods(methods []string) error { + if len(methods) == 0 { + return errors.New("no Options.Methods found") + } + + allowedMethods := []string{ + "/block", + "/block/transaction", + "/mempool", + "/mempool/transaction", + "/account/balance", + "/account/transactions", + "/construction/metadata", + "/construction/submit", + } + + for _, method := range methods { + if !contains(allowedMethods, method) { + return fmt.Errorf("%s is not a valid method", method) + } + } + + return nil +} + +// NetworkInformation ensures any rosetta.NetworkInformation +// included in rosetta.NetworkStatus or rosetta.SubNetworkStatus is valid. +func NetworkInformation(networkInformation *rosetta.NetworkInformation) error { + if networkInformation == nil { + return errors.New("network information is nil") + } + + if err := BlockIdentifier(networkInformation.CurrentBlockIdentifier); err != nil { + return err + } + + if err := Timestamp(networkInformation.CurrentBlockTimestamp); err != nil { + return err + } + + if err := BlockIdentifier(networkInformation.GenesisBlockIdentifier); err != nil { + return err + } + + for _, peer := range networkInformation.Peers { + if err := Peer(peer); err != nil { + return err + } + } + + return nil +} + +// NetworkStatus ensures a rosetta.NetworkStatus object is valid. +func NetworkStatus(networkStatus *rosetta.NetworkStatus) error { + if networkStatus == nil { + return errors.New("network status is nil") + } + + if err := PartialNetworkIdentifier(networkStatus.NetworkIdentifier); err != nil { + return err + } + + if err := NetworkInformation(networkStatus.NetworkInformation); err != nil { + return err + } + + return nil +} + +// OperationStatuses ensures all items in Options.OperationStatuses +// are valid and that there exists at least 1 successful status. +func OperationStatuses(statuses []*rosetta.OperationStatus) error { + if len(statuses) == 0 { + return errors.New("no Options.OperationStatuses found") + } + + foundSuccessful := false + for _, status := range statuses { + if status.Status == "" { + return errors.New("Operation.Status is missing") + } + + if status.Successful { + foundSuccessful = true + } + } + + if !foundSuccessful { + return errors.New("no successful Options.OperationStatuses found") + } + + return nil +} + +// SubmissionStatuses ensures all items in Options.SubmissionStatus +// are valid and that there exists at least 1 successful status. +func SubmissionStatuses(statuses []*rosetta.SubmissionStatus) error { + if len(statuses) == 0 { + return errors.New("no Options.SubmissionStatuses found") + } + + foundSuccessful := false + for _, status := range statuses { + if status.Status == "" { + return errors.New("submission status is missing") + } + + if status.Successful { + foundSuccessful = true + } + } + + if !foundSuccessful { + return errors.New("no successful Options.SubmissionStatuses found") + } + + return nil +} + +// NetworkOptions ensures a rosetta.Options object is valid. +func NetworkOptions(options *rosetta.Options) error { + if options == nil { + return errors.New("options is nil") + } + + if err := SupportedMethods(options.Methods); err != nil { + return err + } + + if err := OperationStatuses(options.OperationStatuses); err != nil { + return err + } + + if err := StringArray("Options.OperationTypes", options.OperationTypes); err != nil { + return err + } + + if err := SubmissionStatuses(options.SubmissionStatuses); err != nil { + return err + } + + return nil +} + +// NetworkStatusResponse orchestrates assertions for all +// components of a rosetta.NetworkStatus. +func NetworkStatusResponse(response *rosetta.NetworkStatusResponse) error { + if err := NetworkStatus(response.NetworkStatus); err != nil { + return err + } + + if response.SubNetworkStatus != nil { + for _, subNetwork := range response.SubNetworkStatus { + if err := SubNetworkStatus(subNetwork); err != nil { + return err + } + } + } + + if err := Version(response.Version); err != nil { + return err + } + + if err := NetworkOptions(response.Options); err != nil { + return err + } + + return nil +} diff --git a/asserter/network_test.go b/asserter/network_test.go new file mode 100644 index 00000000..977d9e7a --- /dev/null +++ b/asserter/network_test.go @@ -0,0 +1,283 @@ +// 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" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" + + "github.com/stretchr/testify/assert" +) + +func TestNetworkIdentifier(t *testing.T) { + var tests = map[string]struct { + network *rosetta.NetworkIdentifier + err error + }{ + "valid network": { + network: &rosetta.NetworkIdentifier{ + Blockchain: "bitcoin", + Network: "mainnet", + }, + err: nil, + }, + "nil network": { + network: nil, + err: errors.New("NetworkIdentifier.Blockchain is missing"), + }, + "invalid blockchain": { + network: &rosetta.NetworkIdentifier{ + Blockchain: "", + Network: "mainnet", + }, + err: errors.New("NetworkIdentifier.Blockchain is missing"), + }, + "invalid network": { + network: &rosetta.NetworkIdentifier{ + Blockchain: "bitcoin", + Network: "", + }, + err: errors.New("NetworkIdentifier.Network is missing"), + }, + "valid sub_network": { + network: &rosetta.NetworkIdentifier{ + Blockchain: "bitcoin", + Network: "mainnet", + SubNetworkIdentifier: &rosetta.SubNetworkIdentifier{ + SubNetwork: "shard 1", + }, + }, + err: nil, + }, + "invalid sub_network": { + network: &rosetta.NetworkIdentifier{ + Blockchain: "bitcoin", + Network: "mainnet", + SubNetworkIdentifier: &rosetta.SubNetworkIdentifier{}, + }, + err: errors.New("NetworkIdentifier.SubNetworkIdentifier.SubNetwork is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := NetworkIdentifier(test.network) + assert.Equal(t, test.err, err) + }) + } +} + +func TestSupportedMethods(t *testing.T) { + var tests = map[string]struct { + methods []string + err error + }{ + "valid method": { + methods: []string{ + "/block", + }, + err: nil, + }, + "invalid method": { + methods: []string{ + "/steal/money", + }, + err: errors.New("/steal/money is not a valid method"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := SupportedMethods(test.methods) + assert.Equal(t, test.err, err) + }) + } +} + +func TestVersion(t *testing.T) { + var ( + middlewareVersion = "1.2" + invalidMiddlewareVersion = "" + validRosettaVersion = "1.2.4" + ) + + var tests = map[string]struct { + version *rosetta.Version + err error + }{ + "valid version": { + version: &rosetta.Version{ + RosettaVersion: validRosettaVersion, + NodeVersion: "1.0", + }, + err: nil, + }, + "valid version with middleware": { + version: &rosetta.Version{ + RosettaVersion: validRosettaVersion, + NodeVersion: "1.0", + MiddlewareVersion: &middlewareVersion, + }, + err: nil, + }, + "nil version": { + version: nil, + err: errors.New("version is nil"), + }, + "invalid RosettaVersion": { + version: &rosetta.Version{ + RosettaVersion: "1.2.2", + NodeVersion: "1.0", + }, + err: errors.New("Version.RosettaVersion 1.2.2 is invalid"), + }, + "invalid NodeVersion": { + version: &rosetta.Version{ + RosettaVersion: validRosettaVersion, + }, + err: errors.New("Version.NodeVersion is missing"), + }, + "invalid MiddlewareVersion": { + version: &rosetta.Version{ + RosettaVersion: validRosettaVersion, + NodeVersion: "1.0", + MiddlewareVersion: &invalidMiddlewareVersion, + }, + err: errors.New("Version.MiddlewareVersion is missing"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := Version(test.version) + assert.Equal(t, test.err, err) + }) + } +} + +func TestNetworkOptions(t *testing.T) { + var ( + methods = []string{ + "/block", + "/account/balance", + } + + operationStatuses = []*rosetta.OperationStatus{ + &rosetta.OperationStatus{ + Status: "SUCCESS", + Successful: true, + }, + &rosetta.OperationStatus{ + Status: "FAILURE", + Successful: false, + }, + } + + operationTypes = []string{ + "PAYMENT", + } + + submissionStatuses = []*rosetta.SubmissionStatus{ + &rosetta.SubmissionStatus{ + Status: "MEMPOOL", + Successful: true, + }, + &rosetta.SubmissionStatus{ + Status: "SIG_FAIL", + Successful: false, + }, + } + ) + + var tests = map[string]struct { + networkOptions *rosetta.Options + err error + }{ + "valid options": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationStatuses: operationStatuses, + OperationTypes: operationTypes, + SubmissionStatuses: submissionStatuses, + }, + }, + "nil options": { + networkOptions: nil, + err: errors.New("options is nil"), + }, + "no methods": { + networkOptions: &rosetta.Options{ + OperationStatuses: operationStatuses, + OperationTypes: operationTypes, + SubmissionStatuses: submissionStatuses, + }, + err: errors.New("no Options.Methods found"), + }, + "no OperationStatuses": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationTypes: operationTypes, + SubmissionStatuses: submissionStatuses, + }, + err: errors.New("no Options.OperationStatuses found"), + }, + "no successful OperationStatuses": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationStatuses: []*rosetta.OperationStatus{ + operationStatuses[1], + }, + OperationTypes: operationTypes, + SubmissionStatuses: submissionStatuses, + }, + err: errors.New("no successful Options.OperationStatuses found"), + }, + "no OperationTypes": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationStatuses: operationStatuses, + SubmissionStatuses: submissionStatuses, + }, + err: errors.New("no Options.OperationTypes found"), + }, + "no SubmissionStatuses": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationStatuses: operationStatuses, + OperationTypes: operationTypes, + }, + err: errors.New("no Options.SubmissionStatuses found"), + }, + "no successful SubmissionStatuses": { + networkOptions: &rosetta.Options{ + Methods: methods, + OperationStatuses: operationStatuses, + OperationTypes: operationTypes, + SubmissionStatuses: []*rosetta.SubmissionStatus{ + submissionStatuses[1], + }, + }, + err: errors.New("no successful Options.SubmissionStatuses found"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.err, NetworkOptions(test.networkOptions)) + }) + } +} diff --git a/codegen.sh b/codegen.sh new file mode 100755 index 00000000..a09ba4ec --- /dev/null +++ b/codegen.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# 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. + +OS="$(uname)" +case "${OS}" in + 'Linux') + OS='linux' + SED_IFLAG=(-i'') + ;; + 'Darwin') + OS='macos' + SED_IFLAG=(-i '') + ;; + *) + echo "Operating system '${OS}' not supported." + exit 1 + ;; +esac + +# Remove existing generated code +rm -rf gen; + +# Generate new 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 \ + --additional-properties packageVersion=0.0.1 \ + -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; + +# 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/*; + +# 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/*; + +# Fix slice containing pointers +sed "${SED_IFLAG[@]}" 's/\*\[\]/\[\]\*/g' gen/*; + +# Format generated code +gofmt -w gen/; + +# Ensure license correct +make add-license; diff --git a/fetcher/account.go b/fetcher/account.go new file mode 100644 index 00000000..395ec18c --- /dev/null +++ b/fetcher/account.go @@ -0,0 +1,94 @@ +// 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 fetcher + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// 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) { + balance, _, err := f.rosettaClient.AccountAPI.AccountBalance(ctx, + rosetta.AccountBalanceRequest{ + NetworkIdentifier: network, + AccountIdentifier: account, + }, + ) + if err != nil { + return nil, nil, err + } + + return balance.BlockIdentifier, balance.Balances, nil +} + +// AccountBalance returns the validated response +// from the AccountBalance method. +func (f *Fetcher) AccountBalance( + ctx context.Context, + network *rosetta.NetworkIdentifier, + account *rosetta.AccountIdentifier, +) (*rosetta.BlockIdentifier, []*rosetta.Balance, error) { + block, balances, err := f.UnsafeAccountBalance(ctx, network, account) + if err != nil { + return nil, nil, err + } + + if err := asserter.AccountBalance(block, balances); err != nil { + return nil, nil, err + } + + return block, balances, nil +} + +// AccountBalanceRetry retrieves the validated AccountBalance +// with a specified number of retries and max elapsed time. +func (f *Fetcher) AccountBalanceRetry( + ctx context.Context, + network *rosetta.NetworkIdentifier, + account *rosetta.AccountIdentifier, + maxElapsedTime time.Duration, + maxRetries uint64, +) (*rosetta.BlockIdentifier, []*rosetta.Balance, error) { + backoffRetries := backoffRetries(maxElapsedTime, maxRetries) + + for ctx.Err() == nil { + block, balances, err := f.AccountBalance( + ctx, + network, + account, + ) + if err == nil { + return block, balances, nil + } + + if !tryAgain(fmt.Sprintf("account %s", account.Address), backoffRetries, err) { + break + } + } + + return nil, nil, errors.New("exhausted retries for account") +} diff --git a/fetcher/block.go b/fetcher/block.go new file mode 100644 index 00000000..9865e3a5 --- /dev/null +++ b/fetcher/block.go @@ -0,0 +1,333 @@ +// 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 fetcher + +import ( + "context" + "errors" + "fmt" + "time" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" + + "golang.org/x/sync/errgroup" +) + +// addTransactionIdentifiers appends a slice of +// rosetta.TransactionIdentifiers to a channel. +// When all rosetta.TransactionIdentifiers are added, +// the channel is closed. +func addTransactionIdentifiers( + ctx context.Context, + txsToFetch chan *rosetta.TransactionIdentifier, + identifiers []*rosetta.TransactionIdentifier, +) error { + defer close(txsToFetch) + for _, txHash := range identifiers { + select { + case txsToFetch <- txHash: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +// fetchChannelTransactions fetches transactions from a +// channel until there are no more transactions in the +// 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, +) error { + for transactionIdentifier := range txsToFetch { + tx, _, err := f.rosettaClient.BlockAPI.BlockTransaction(ctx, + rosetta.BlockTransactionRequest{ + NetworkIdentifier: network, + BlockIdentifier: block, + TransactionIdentifier: transactionIdentifier, + }, + ) + + if err != nil { + return err + } + + select { + case fetchedTxs <- tx.Transaction: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +// UnsafeTransactions returns the unvalidated response +// from the BlockTransaction method. UnsafeTransactions +// fetches all provided rosetta.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) { + if transactionIdentifiers == nil || len(transactionIdentifiers) == 0 { + return nil, nil + } + + txsToFetch := make(chan *rosetta.TransactionIdentifier) + fetchedTxs := make(chan *rosetta.Transaction) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return addTransactionIdentifiers(ctx, txsToFetch, transactionIdentifiers) + }) + + for i := uint64(0); i < f.transactionConcurrency; i++ { + g.Go(func() error { + return f.fetchChannelTransactions(ctx, network, block, txsToFetch, fetchedTxs) + }) + } + + go func() { + _ = g.Wait() + close(fetchedTxs) + }() + + txs := make([]*rosetta.Transaction, 0) + for tx := range fetchedTxs { + txs = append(txs, tx) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + return txs, nil +} + +// UnsafeBlock returns the unvalidated response +// from the Block method. This function will +// automatically fetch any transactions that +// were not returned by the call to fetch the +// 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{ + NetworkIdentifier: network, + BlockIdentifier: blockIdentifier, + }) + if err != nil { + return nil, err + } + + // Exit early if no need to fetch txs + if blockResponse.OtherTransactions == nil || len(blockResponse.OtherTransactions) == 0 { + return blockResponse.Block, nil + } + + batchFetch, err := f.UnsafeTransactions( + ctx, + network, + blockResponse.Block.BlockIdentifier, + blockResponse.OtherTransactions, + ) + if err != nil { + return nil, err + } + + blockResponse.Block.Transactions = append(blockResponse.Block.Transactions, batchFetch...) + + return blockResponse.Block, nil +} + +// Block returns the validated response from +// the block method. This function will +// automatically fetch any transactions that +// were not returned by the call to fetch the +// block. +func (f *Fetcher) Block( + ctx context.Context, + network *rosetta.NetworkIdentifier, + blockIdentifier *rosetta.PartialBlockIdentifier, +) (*rosetta.Block, error) { + if f.Asserter == nil { + return nil, errors.New("asserter not initialized") + } + + block, err := f.UnsafeBlock(ctx, network, blockIdentifier) + if err != nil { + return nil, err + } + + if err := f.Asserter.Block(ctx, block); err != nil { + return nil, err + } + + return block, nil +} + +// BlockRetry retrieves a validated Block +// with a specified number of retries and max elapsed time. +func (f *Fetcher) BlockRetry( + ctx context.Context, + network *rosetta.NetworkIdentifier, + blockIdentifier *rosetta.PartialBlockIdentifier, + maxElapsedTime time.Duration, + maxRetries uint64, +) (*rosetta.Block, error) { + if f.Asserter == nil { + return nil, errors.New("asserter not initialized") + } + + backoffRetries := backoffRetries(maxElapsedTime, maxRetries) + + for ctx.Err() == nil { + block, err := f.Block( + ctx, + network, + blockIdentifier, + ) + if err == nil { + return block, nil + } + + if !tryAgain(fmt.Sprintf("block %d", blockIdentifier.Index), backoffRetries, err) { + break + } + } + + return nil, errors.New("exhausted retries for block") +} + +// BlockAndLatency is utilized to track the latency +// of concurrent block fetches. +type BlockAndLatency struct { + Block *rosetta.Block + Latency float64 +} + +// addIndicies appends a range of indicies (from +// startIndex to endIndex, inclusive) to the +// blockIndicies channel. When all indicies are added, +// the channel is closed. +func addBlockIndicies( + ctx context.Context, + blockIndicies chan int64, + startIndex int64, + endIndex int64, +) error { + defer close(blockIndicies) + for i := startIndex; i <= endIndex; i++ { + select { + case blockIndicies <- i: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// fetchChannelBlocks fetches blocks from a +// channel with retries until there are no +// more blocks in the channel or there is an +// error. +func (f *Fetcher) fetchChannelBlocks( + ctx context.Context, + network *rosetta.NetworkIdentifier, + blockIndicies chan int64, + results chan *BlockAndLatency, +) error { + for b := range blockIndicies { + start := time.Now() + block, err := f.BlockRetry( + ctx, + network, + &rosetta.PartialBlockIdentifier{ + Index: &b, + }, + DefaultElapsedTime, + DefaultRetries, + ) + if err != nil { + return err + } + + select { + case results <- &BlockAndLatency{ + Block: block, + Latency: time.Since(start).Seconds(), + }: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +// BlockRange concurrently fetches blocks from startIndex to endIndex, +// inclusive. Blocks returned by this method may not contain a path +// from the endBlock to the startBlock over Block.ParentBlockIdentifers +// if a re-org occurs during the fetch. This should be handled gracefully +// by any callers. +func (f *Fetcher) BlockRange( + ctx context.Context, + network *rosetta.NetworkIdentifier, + startIndex int64, + endIndex int64, +) (map[int64]*BlockAndLatency, error) { + blockIndicies := make(chan int64) + results := make(chan *BlockAndLatency) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return addBlockIndicies(ctx, blockIndicies, startIndex, endIndex) + }) + + for j := uint64(0); j < f.blockConcurrency; j++ { + g.Go(func() error { + return f.fetchChannelBlocks(ctx, network, blockIndicies, results) + }) + } + + // Wait for all block fetching goroutines to exit + // before closing the results channel. + go func() { + _ = g.Wait() + close(results) + }() + + m := make(map[int64]*BlockAndLatency) + for b := range results { + m[b.Block.BlockIdentifier.Index] = b + } + + err := g.Wait() + if err != nil { + return nil, err + } + + return m, nil +} diff --git a/fetcher/construction.go b/fetcher/construction.go new file mode 100644 index 00000000..bbf2fb9d --- /dev/null +++ b/fetcher/construction.go @@ -0,0 +1,76 @@ +// 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 fetcher + +import ( + "context" + "errors" + + "github.com/coinbase/rosetta-sdk-go/asserter" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// ConstructionMetadata returns the validated response +// from the ConstructionMetadata method. +func (f *Fetcher) ConstructionMetadata( + ctx context.Context, + client *rosetta.APIClient, + network *rosetta.NetworkIdentifier, + account *rosetta.AccountIdentifier, + method *string, +) (*rosetta.Amount, *map[string]interface{}, error) { + metadata, _, err := client.ConstructionAPI.TransactionConstruction(ctx, + rosetta.TransactionConstructionRequest{ + NetworkIdentifier: network, + AccountIdentifier: account, + Method: method, + }, + ) + if err != nil { + return nil, nil, err + } + + if err := asserter.TransactionConstruction(metadata); err != nil { + return nil, nil, err + } + + return metadata.SuggestedFee, metadata.Metadata, nil +} + +// ConstructionSubmit returns the validated response +// from the ConstructionSubmit method. +func (f *Fetcher) ConstructionSubmit( + ctx context.Context, + client *rosetta.APIClient, + signedTransaction string, +) (*rosetta.TransactionIdentifier, *string, *map[string]interface{}, error) { + if f.Asserter == nil { + return nil, nil, nil, errors.New("asserter not initialized") + } + + submitResponse, _, err := client.ConstructionAPI.TransactionSubmit(ctx, rosetta.TransactionSubmitRequest{ + SignedTransaction: signedTransaction, + }) + if err != nil { + return nil, nil, nil, err + } + + if err := f.Asserter.TransactionSubmit(submitResponse); err != nil { + return nil, nil, nil, err + } + + return submitResponse.TransactionIdentifier, &submitResponse.Status, submitResponse.Metadata, nil +} diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go new file mode 100644 index 00000000..801f1932 --- /dev/null +++ b/fetcher/fetcher.go @@ -0,0 +1,104 @@ +// 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 fetcher + +import ( + "context" + "net/http" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +const ( + // DefaultElapsedTime is the default limit on time + // spent retrying a fetch. + DefaultElapsedTime = 1 * time.Minute + + // DefaultRetries is the default number of times to + // attempt a retry on a failed request. + DefaultRetries = 10 + + // DefaultBlockConcurrency is the default number of + // blocks a Fetcher will try to get concurrently. + DefaultBlockConcurrency = 8 + + // DefaultTransactionConcurrency is the default + // number of transactions a Fetcher will try to + // get concurrently when populating a block (if + // transactions are not included in the original + // block fetch). + DefaultTransactionConcurrency = 8 +) + +// Fetcher contains all logic to communicate with a Rosetta Server. +type Fetcher struct { + // Asserter is a public variable because + // it can be used to determine if a retrieved + // rosetta.Operation is successful and should + // be applied. + Asserter *asserter.Asserter + rosettaClient *rosetta.APIClient + blockConcurrency uint64 + transactionConcurrency uint64 +} + +// New constructs a new Fetcher. +func New( + ctx context.Context, + serverAddress string, + userAgent string, + httpClient *http.Client, + blockConcurrency uint64, + transactionConcurrency uint64, +) *Fetcher { + clientCfg := rosetta.NewConfiguration(serverAddress, userAgent, httpClient) + client := rosetta.NewAPIClient(clientCfg) + + return &Fetcher{ + rosettaClient: client, + blockConcurrency: blockConcurrency, + transactionConcurrency: transactionConcurrency, + } +} + +// InitializeAsserter creates an Asserter for +// validating responses. The Asserter is created +// from a rosetta.NetworkStatusResponse. This +// method should be called before making any +// validated client requests. +func (f *Fetcher) InitializeAsserter( + ctx context.Context, +) (*rosetta.NetworkStatusResponse, error) { + // Attempt to fetch network status + networkResponse, err := f.NetworkStatusRetry( + ctx, + nil, + DefaultElapsedTime, + DefaultRetries, + ) + if err != nil { + return nil, err + } + + f.Asserter = asserter.New( + ctx, + networkResponse, + ) + + return networkResponse, nil +} diff --git a/fetcher/mempool.go b/fetcher/mempool.go new file mode 100644 index 00000000..6a2d8656 --- /dev/null +++ b/fetcher/mempool.go @@ -0,0 +1,101 @@ +// 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 fetcher + +import ( + "context" + "errors" + + "github.com/coinbase/rosetta-sdk-go/asserter" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// 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, + }) + if err != nil { + return nil, err + } + + return mempool.TransactionIdentifiers, nil +} + +// Mempool returns the validated response +// from the Mempool method. +func (f *Fetcher) Mempool( + ctx context.Context, + network *rosetta.NetworkIdentifier, +) ([]*rosetta.TransactionIdentifier, error) { + mempool, err := f.UnsafeMempool(ctx, network) + if err != nil { + return nil, err + } + + if err := asserter.MempoolTransactions(mempool); err != nil { + return nil, err + } + + return mempool, nil +} + +// UnsafeMempoolTransaction returns the unvalidated response +// 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{ + NetworkIdentifier: network, + TransactionIdentifier: transaction, + }, + ) + if err != nil { + return nil, nil, err + } + + return mempoolTransaction.Transaction, mempoolTransaction.Metadata, nil +} + +// MempoolTransaction returns the validated response +// from the MempoolTransaction method. +func (f *Fetcher) MempoolTransaction( + ctx context.Context, + network *rosetta.NetworkIdentifier, + transaction *rosetta.TransactionIdentifier, +) (*rosetta.Transaction, *map[string]interface{}, error) { + if f.Asserter == nil { + return nil, nil, errors.New("asserter not initialized") + } + + mempoolTransaction, metadata, err := f.UnsafeMempoolTransaction(ctx, network, transaction) + if err != nil { + return nil, nil, err + } + + if err := f.Asserter.Transaction(mempoolTransaction); err != nil { + return nil, nil, err + } + + return mempoolTransaction, metadata, nil +} diff --git a/fetcher/network.go b/fetcher/network.go new file mode 100644 index 00000000..027d0ac5 --- /dev/null +++ b/fetcher/network.go @@ -0,0 +1,86 @@ +// 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 fetcher + +import ( + "context" + "errors" + "time" + + "github.com/coinbase/rosetta-sdk-go/asserter" + + rosetta "github.com/coinbase/rosetta-sdk-go/gen" +) + +// UnsafeNetworkStatus returns the unvalidated response +// from the NetworkStatus method. +func (f *Fetcher) UnsafeNetworkStatus( + ctx context.Context, + metadata *map[string]interface{}, +) (*rosetta.NetworkStatusResponse, error) { + networkStatus, _, err := f.rosettaClient.NetworkAPI.NetworkStatus( + ctx, + rosetta.NetworkStatusRequest{ + Metadata: metadata, + }, + ) + if err != nil { + return nil, err + } + + return networkStatus, nil +} + +// NetworkStatus returns the validated response +// from the NetworkStatus method. +func (f *Fetcher) NetworkStatus( + ctx context.Context, + metadata *map[string]interface{}, +) (*rosetta.NetworkStatusResponse, error) { + networkStatus, err := f.UnsafeNetworkStatus(ctx, metadata) + if err != nil { + return nil, err + } + + if err := asserter.NetworkStatusResponse(networkStatus); err != nil { + return nil, err + } + + return networkStatus, nil +} + +// NetworkStatusRetry retrieves the validated NetworkStatus +// with a specified number of retries and max elapsed time. +func (f *Fetcher) NetworkStatusRetry( + ctx context.Context, + metadata *map[string]interface{}, + maxElapsedTime time.Duration, + maxRetries uint64, +) (*rosetta.NetworkStatusResponse, error) { + backoffRetries := backoffRetries(maxElapsedTime, maxRetries) + + for ctx.Err() == nil { + networkStatus, err := f.NetworkStatus(ctx, metadata) + if err == nil { + return networkStatus, nil + } + + if !tryAgain("network", backoffRetries, err) { + break + } + } + + return nil, errors.New("exhausted retries for network") +} diff --git a/fetcher/utils.go b/fetcher/utils.go new file mode 100644 index 00000000..bd7360e4 --- /dev/null +++ b/fetcher/utils.go @@ -0,0 +1,49 @@ +// 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 fetcher + +import ( + "log" + "time" + + "github.com/cenkalti/backoff" +) + +// backoffRetries creates the backoff.BackOff struct used by all +// *Retry functions in the fetcher. +func backoffRetries( + maxElapsedTime time.Duration, + maxRetries uint64, +) backoff.BackOff { + exponentialBackoff := backoff.NewExponentialBackOff() + exponentialBackoff.MaxElapsedTime = maxElapsedTime + return backoff.WithMaxRetries(exponentialBackoff, maxRetries) +} + +// tryAgain handles a backoff and prints error messages depending +// on the fetchMsg. +func tryAgain(fetchMsg string, thisBackoff backoff.BackOff, err error) bool { + log.Printf("%s fetch error: %s\n", fetchMsg, err.Error()) + + nextBackoff := thisBackoff.NextBackOff() + if nextBackoff == backoff.Stop { + return false + } + + log.Printf("retrying fetch for %s after %fs\n", fetchMsg, nextBackoff.Seconds()) + time.Sleep(nextBackoff) + + return true +} diff --git a/gen/api_account.go b/gen/api_account.go new file mode 100644 index 00000000..79b15ac5 --- /dev/null +++ b/gen/api_account.go @@ -0,0 +1,105 @@ +// 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 gen + +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" +) + +// Linger please +var ( + _ _context.Context +) + +// AccountAPIService AccountAPI service +type AccountAPIService service + +/* +AccountBalance Get an Account Balance +Get an array of all Account Balances for an Account Identifier and the Block Identifier at which the balance lookup was performed. Some consumers of account balance data need to know at which block the balance was calculated to reconcile account balance changes. To get all balances associated with an account, it may be necessary to perform multiple balance requests with unique Account Identifiers. If the client supports it, passing nil AccountIdentifier metadata to the request should fetch all balances. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param accountBalanceRequest +@return AccountBalanceResponse +*/ +func (a *AccountAPIService) AccountBalance(ctx _context.Context, accountBalanceRequest AccountBalanceRequest) (*AccountBalanceResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/account/balance" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &accountBalanceRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} diff --git a/gen/api_block.go b/gen/api_block.go new file mode 100644 index 00000000..bd791fbd --- /dev/null +++ b/gen/api_block.go @@ -0,0 +1,179 @@ +// 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 gen + +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" +) + +// Linger please +var ( + _ _context.Context +) + +// BlockAPIService BlockAPI service +type BlockAPIService service + +/* +Block Get a Block +Get a block by its Block Identifier If transactions are returned in the same call to the node as fetching the block, the response should include these transactions in the Block object. If not, an array of Transaction Identifiers should be returned so /block/transaction fetches can be done to get all transaction information. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param blockRequest +@return BlockResponse +*/ +func (a *BlockAPIService) Block(ctx _context.Context, blockRequest BlockRequest) (*BlockResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/block" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &blockRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} + +/* +BlockTransaction Get a Block Transaction +Get a transaction in a block by its Transaction Identifier This method should only be used when querying a node for a block does not return all transactions contained within it. All transactions returned by this method must be appended to any transactions returned by the /block method by consumers of this data. Fetching a transaction by hash is considered an \Explorer Method\ (which is classified under the \Future Work\ section). Calling this method requires reference to a BlockIdentifier because transaction parsing can change depending on which block contains the transaction. For example, in Bitcoin it is necessary to know which block contains a transaction to determine the destination of fee payments. Without specifying a block identifier, the node would have to infer which block to use (which could change during a re-org). Implementations that require fetching previous transactions to populate the response (ex: Previous UTXOs in Bitcoin) may find it useful to run a cache within the Rosetta server in the /data directory (on a path that does not conflict with the node). + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param blockTransactionRequest +@return BlockTransactionResponse +*/ +func (a *BlockAPIService) BlockTransaction(ctx _context.Context, blockTransactionRequest BlockTransactionRequest) (*BlockTransactionResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/block/transaction" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &blockTransactionRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} diff --git a/gen/api_construction.go b/gen/api_construction.go new file mode 100644 index 00000000..7dead699 --- /dev/null +++ b/gen/api_construction.go @@ -0,0 +1,179 @@ +// 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 gen + +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" +) + +// Linger please +var ( + _ _context.Context +) + +// ConstructionAPIService ConstructionAPI service +type ConstructionAPIService service + +/* +TransactionConstruction Get Transaction Construction Metadata +Get any information required to construct a transaction for a specific account. Metadata returned here could be a recent hash to use or an account sequence number. It is important to clarify that this endpoint should not pre-construct any transactions for the client. All \account-specific\ metadata must be returned as a key-value mapping so that transaction construction can be audited and performed entirely offline. Any \account-agnostic\ metadata does not need to be broken out into a key-value mapping and can be returned as a blob. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param transactionConstructionRequest +@return TransactionConstructionResponse +*/ +func (a *ConstructionAPIService) TransactionConstruction(ctx _context.Context, transactionConstructionRequest TransactionConstructionRequest) (*TransactionConstructionResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/construction/metadata" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &transactionConstructionRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} + +/* +TransactionSubmit Submit a Signed Transaction +Submit a signed transaction in network-specific format + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param transactionSubmitRequest +@return TransactionSubmitResponse +*/ +func (a *ConstructionAPIService) TransactionSubmit(ctx _context.Context, transactionSubmitRequest TransactionSubmitRequest) (*TransactionSubmitResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/construction/submit" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &transactionSubmitRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} diff --git a/gen/api_mempool.go b/gen/api_mempool.go new file mode 100644 index 00000000..18adc4f3 --- /dev/null +++ b/gen/api_mempool.go @@ -0,0 +1,179 @@ +// 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 gen + +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" +) + +// Linger please +var ( + _ _context.Context +) + +// MempoolAPIService MempoolAPI service +type MempoolAPIService service + +/* +Mempool Get All Mempool Transactions +Get all Transaction Identifiers in the mempool + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param mempoolRequest +@return MempoolResponse +*/ +func (a *MempoolAPIService) Mempool(ctx _context.Context, mempoolRequest MempoolRequest) (*MempoolResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/mempool" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &mempoolRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} + +/* +MempoolTransaction Get a Mempool Transaction +Get a transaction in the mempool by its Transaction Identifier. This is a separate request than fetching a block transaction (/block/transaction) because some blockchain nodes need to know that a transaction query is for something in the mempool instead of a transaction in a block. Transactions may not be fully parsable until they are in a block (ex: may not be possible to determine the fee to pay before a transaction is executed). On this endpoint, it is ok that returned transactions are only estimates of what may actually be included in a block. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param mempoolTransactionRequest +@return MempoolTransactionResponse +*/ +func (a *MempoolAPIService) MempoolTransaction(ctx _context.Context, mempoolTransactionRequest MempoolTransactionRequest) (*MempoolTransactionResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/mempool/transaction" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &mempoolTransactionRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} diff --git a/gen/api_network.go b/gen/api_network.go new file mode 100644 index 00000000..53aa2175 --- /dev/null +++ b/gen/api_network.go @@ -0,0 +1,105 @@ +// 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 gen + +import ( + _context "context" + _ioutil "io/ioutil" + _nethttp "net/http" +) + +// Linger please +var ( + _ _context.Context +) + +// NetworkAPIService NetworkAPI service +type NetworkAPIService service + +/* +NetworkStatus Get Network Status +This method returns the current status of the network the node knows about. This method also returns the methods, operation types, and operation statuses the node supports. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param networkStatusRequest There are not any required fields in this request, yet. It is still specified as a `POST` request to ensure required fields can be added without requiring clients to change from a `GET`(which is currently more ideal) to a `POST` request. +@return NetworkStatusResponse +*/ +func (a *NetworkAPIService) NetworkStatus(ctx _context.Context, networkStatusRequest NetworkStatusRequest) (*NetworkStatusResponse, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + ) + + // create path and map variables + localVarPath := a.client.cfg.BasePath + "/network/status" + localVarHeaderParams := make(map[string]string) + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = &networkStatusRequest + + r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(ctx, r) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + defer localVarHTTPResponse.Body.Close() + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode != 200 { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return nil, localVarHTTPResponse, newErr + } + + var v 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 &v, localVarHTTPResponse, nil +} diff --git a/gen/client.go b/gen/client.go new file mode 100644 index 00000000..12062916 --- /dev/null +++ b/gen/client.go @@ -0,0 +1,370 @@ +// 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 gen + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "reflect" + "regexp" + "strings" + "time" +) + +const ( + // APIVersion is the version of the Rosetta API Spec + // used to generate this code. + APIVersion = "1.2.4" +) + +var ( + jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) +) + +// APIClient manages communication with the Rosetta API v1.2.4 +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services + + AccountAPI *AccountAPIService + + BlockAPI *BlockAPIService + + ConstructionAPI *ConstructionAPIService + + MempoolAPI *MempoolAPIService + + NetworkAPI *NetworkAPIService +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + c.common.client = c + + // API Services + c.AccountAPI = (*AccountAPIService)(&c.common) + c.BlockAPI = (*BlockAPIService)(&c.common) + c.ConstructionAPI = (*ConstructionAPIService)(&c.common) + c.MempoolAPI = (*MempoolAPIService)(&c.common) + c.NetworkAPI = (*NetworkAPIService)(&c.common) + + return c +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insenstive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.ToLower(a) == strings.ToLower(needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +// parameterToString convert interface{} parameters to string, using a delimiter if format is provided. +func parameterToString(obj interface{}, collectionFormat string) string { + var delimiter string + + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") + } else if t, ok := obj.(time.Time); ok { + return t.Format(time.RFC3339) + } + + return fmt.Sprintf("%v", obj) +} + +// helper for converting interface{} parameters to json strings +func parameterToJSON(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + +// callAPI do the request. +func (c *APIClient) callAPI(ctx context.Context, request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request.WithContext(ctx)) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + + return resp, err +} + +// ChangeBasePath changes base path to allow switching to mocks +func (c *APIClient) ChangeBasePath(path string) { + c.cfg.BasePath = path +} + +// GetConfig allows for modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, +) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Generate a new request + localVarRequest, err = http.NewRequest(method, url.String(), body) + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers.Set(h, v) + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } + + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if jsonCheck.MatchString(contentType) { + if err = json.Unmarshal(b, v); err != nil { + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if jsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("invalid body type %s", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + 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/gen/configuration.go new file mode 100644 index 00000000..1b06b269 --- /dev/null +++ b/gen/configuration.go @@ -0,0 +1,140 @@ +// 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 gen + +import ( + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request. + ContextOAuth2 = contextKey("token") + + // ContextBasicAuth takes BasicAuth as authentication for the request. + ContextBasicAuth = contextKey("basic") + + // ContextAccessToken takes a string oauth2 access token as authentication for the request. + ContextAccessToken = contextKey("accesstoken") + + // ContextAPIKey takes an APIKey as authentication for the request + ContextAPIKey = contextKey("apikey") +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + URL string + Description string + Variables map[string]ServerVariable +} + +// Configuration stores the configuration of the API client +type Configuration struct { + BasePath string `json:"basePath,omitempty"` + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers []ServerConfiguration + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration(basePath string, userAgent string, httpClient *http.Client) *Configuration { + cfg := &Configuration{ + BasePath: basePath, + DefaultHeader: make(map[string]string), + UserAgent: userAgent, + Debug: false, + Servers: []ServerConfiguration{ + { + URL: basePath, + Description: "No description provided", + }, + }, + } + + if httpClient != nil { + cfg.HTTPClient = httpClient + } + + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// ServerURL returns URL based on server settings +func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { + if index < 0 || len(c.Servers) <= index { + return "", fmt.Errorf("Index %v out of range %v", index, len(c.Servers)-1) + } + server := c.Servers[index] + url := server.URL + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("The variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} diff --git a/gen/model_account_balance_request.go b/gen/model_account_balance_request.go new file mode 100644 index 00000000..44db1176 --- /dev/null +++ b/gen/model_account_balance_request.go @@ -0,0 +1,23 @@ +// 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 gen + +// AccountBalanceRequest struct for AccountBalanceRequest +type AccountBalanceRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + AccountIdentifier *AccountIdentifier `json:"account_identifier"` +} diff --git a/gen/model_account_balance_response.go b/gen/model_account_balance_response.go new file mode 100644 index 00000000..64786c3c --- /dev/null +++ b/gen/model_account_balance_response.go @@ -0,0 +1,24 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// AccountBalanceResponse struct for AccountBalanceResponse +type AccountBalanceResponse struct { + BlockIdentifier *BlockIdentifier `json:"block_identifier"` + // A GetAccountBalanceResponse may include multiple uniquely-identified balances. For example, the balance of an account on each shard could be returned or the balance of an account on each ERC-20 contract. + Balances []*Balance `json:"balances"` +} diff --git a/gen/model_account_identifier.go b/gen/model_account_identifier.go new file mode 100644 index 00000000..cbeeaf33 --- /dev/null +++ b/gen/model_account_identifier.go @@ -0,0 +1,26 @@ +// 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 gen + +// 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 `metadata` field, if populated). +type AccountIdentifier struct { + // The `address` may be a cryptographic public key (or some encoding of it) or a provided username. + Address string `json:"address"` + SubAccount *SubAccountIdentifier `json:"sub_account,omitempty"` + // Blockchains that utilize a username model (where the address is not a derivative of a cryptographic public key) should specify the public key(s) owned by the address in metadata. + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_amount.go b/gen/model_amount.go new file mode 100644 index 00000000..a7c82074 --- /dev/null +++ b/gen/model_amount.go @@ -0,0 +1,25 @@ +// 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 gen + +// Amount Amount is some Value of a Currency. It is considered invalid to specify a Value without a Currency. +type Amount struct { + // Value of the transaction in atomic units represented as an arbitrary-sized signed integer. For example, 1 BTC would be represented by a value of 100000000. + Value string `json:"value"` + Currency *Currency `json:"currency"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_balance.go b/gen/model_balance.go new file mode 100644 index 00000000..847017d0 --- /dev/null +++ b/gen/model_balance.go @@ -0,0 +1,26 @@ +// 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 gen + +// 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). +type Balance struct { + AccountIdentifier *AccountIdentifier `json:"account_identifier"` + // A single account may have a balance in multiple currencies. + Amounts []*Amount `json:"amounts"` + // Account-based blockchains that utilize a nonce or sequence number should include that number in the metadata. This number could be unique to the identifier or global across the account address. + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_block.go b/gen/model_block.go new file mode 100644 index 00000000..e2414d21 --- /dev/null +++ b/gen/model_block.go @@ -0,0 +1,27 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// Block Blocks contain an array of Transactions that occured at a particular BlockIdentifier. +type Block struct { + BlockIdentifier *BlockIdentifier `json:"block_identifier"` + ParentBlockIdentifier *BlockIdentifier `json:"parent_block_identifier"` + // The timestamp of the block in milliseconds since the Unix Epoch. The timestamp is stored in milliseconds because some blockchains produce blocks more often than once a second. + Timestamp int64 `json:"timestamp"` + Transactions []*Transaction `json:"transactions"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_block_identifier.go b/gen/model_block_identifier.go new file mode 100644 index 00000000..5f3d0fba --- /dev/null +++ b/gen/model_block_identifier.go @@ -0,0 +1,24 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// BlockIdentifier The `block_identifier` uniquely identifies a block in a particular network. +type BlockIdentifier struct { + // This is also known as the block height. + Index int64 `json:"index"` + Hash string `json:"hash"` +} diff --git a/gen/model_block_request.go b/gen/model_block_request.go new file mode 100644 index 00000000..ea1d324b --- /dev/null +++ b/gen/model_block_request.go @@ -0,0 +1,23 @@ +// 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 gen + +// BlockRequest struct for BlockRequest +type BlockRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + BlockIdentifier *PartialBlockIdentifier `json:"block_identifier"` +} diff --git a/gen/model_block_response.go b/gen/model_block_response.go new file mode 100644 index 00000000..03b23365 --- /dev/null +++ b/gen/model_block_response.go @@ -0,0 +1,24 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// BlockResponse struct for BlockResponse +type BlockResponse struct { + Block *Block `json:"block"` + // Some blockchains may require additional transactions to be fetched that weren't returned in the block response (ex: block only returns transaction hashes). For blockchains with a lot of transactions in each block, this can be very useful as consumers can concurrently fetch all transactions returned. + OtherTransactions []*TransactionIdentifier `json:"other_transactions,omitempty"` +} diff --git a/gen/model_block_transaction_request.go b/gen/model_block_transaction_request.go new file mode 100644 index 00000000..f1a3741b --- /dev/null +++ b/gen/model_block_transaction_request.go @@ -0,0 +1,24 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// BlockTransactionRequest struct for BlockTransactionRequest +type BlockTransactionRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + BlockIdentifier *BlockIdentifier `json:"block_identifier"` + TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` +} diff --git a/gen/model_block_transaction_response.go b/gen/model_block_transaction_response.go new file mode 100644 index 00000000..8b3bda2b --- /dev/null +++ b/gen/model_block_transaction_response.go @@ -0,0 +1,22 @@ +// 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 gen + +// BlockTransactionResponse struct for BlockTransactionResponse +type BlockTransactionResponse struct { + Transaction *Transaction `json:"transaction"` +} diff --git a/gen/model_currency.go b/gen/model_currency.go new file mode 100644 index 00000000..2fdbf6a4 --- /dev/null +++ b/gen/model_currency.go @@ -0,0 +1,27 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// Currency Currency is composed of a cannonical Symbol and Decimals. This Decimals value is used to convert an Amount.Value from atomic units (Satoshis) to standard units (Bitcoins). +type Currency struct { + // Cannonical symbol associated with a currency. + Symbol string `json:"symbol"` + // Number of decimal places in the standard unit representation of the amount. For example, BTC has 8 decimals. Note that it is not possible to represent the value of some currency in atomic units that is not base 10. + Decimals int32 `json:"decimals"` + // Any additional information related to the currency itself. For example, it would be useful to populate this object with the contract address of an ERC-20 token. + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_error.go b/gen/model_error.go new file mode 100644 index 00000000..88edc66d --- /dev/null +++ b/gen/model_error.go @@ -0,0 +1,23 @@ +// 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 gen + +// Error struct for Error +type Error struct { + Code int32 `json:"code"` + Message string `json:"message"` +} diff --git a/gen/model_mempool_request.go b/gen/model_mempool_request.go new file mode 100644 index 00000000..8c74c896 --- /dev/null +++ b/gen/model_mempool_request.go @@ -0,0 +1,22 @@ +// 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 gen + +// MempoolRequest struct for MempoolRequest +type MempoolRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` +} diff --git a/gen/model_mempool_response.go b/gen/model_mempool_response.go new file mode 100644 index 00000000..f87cb407 --- /dev/null +++ b/gen/model_mempool_response.go @@ -0,0 +1,22 @@ +// 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 gen + +// MempoolResponse struct for MempoolResponse +type MempoolResponse struct { + TransactionIdentifiers []*TransactionIdentifier `json:"transaction_identifiers"` +} diff --git a/gen/model_mempool_transaction_request.go b/gen/model_mempool_transaction_request.go new file mode 100644 index 00000000..9de64a7d --- /dev/null +++ b/gen/model_mempool_transaction_request.go @@ -0,0 +1,23 @@ +// 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 gen + +// MempoolTransactionRequest struct for MempoolTransactionRequest +type MempoolTransactionRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` +} diff --git a/gen/model_mempool_transaction_response.go b/gen/model_mempool_transaction_response.go new file mode 100644 index 00000000..df634967 --- /dev/null +++ b/gen/model_mempool_transaction_response.go @@ -0,0 +1,23 @@ +// 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 gen + +// MempoolTransactionResponse struct for MempoolTransactionResponse +type MempoolTransactionResponse struct { + Transaction *Transaction `json:"transaction"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_network_identifier.go b/gen/model_network_identifier.go new file mode 100644 index 00000000..179de770 --- /dev/null +++ b/gen/model_network_identifier.go @@ -0,0 +1,25 @@ +// 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 gen + +// NetworkIdentifier The `network_identifier` specifies which network a particular object is associated with. +type NetworkIdentifier struct { + Blockchain string `json:"blockchain"` + // If a blockchain has a specific `chain-id` or network identifier, it should go in this field. It is up to the client to determine which network-specific identifier is `mainnet` or `testnet`. + Network string `json:"network"` + SubNetworkIdentifier *SubNetworkIdentifier `json:"sub_network_identifier,omitempty"` +} diff --git a/gen/model_network_information.go b/gen/model_network_information.go new file mode 100644 index 00000000..3a46807d --- /dev/null +++ b/gen/model_network_information.go @@ -0,0 +1,26 @@ +// 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 gen + +// NetworkInformation struct for NetworkInformation +type NetworkInformation struct { + CurrentBlockIdentifier *BlockIdentifier `json:"current_block_identifier"` + // The timestamp of the block in milliseconds since the Unix Epoch. The timestamp is stored in milliseconds because some blockchains produce blocks more often than once a second. + CurrentBlockTimestamp int64 `json:"current_block_timestamp"` + GenesisBlockIdentifier *BlockIdentifier `json:"genesis_block_identifier"` + Peers []*Peer `json:"peers"` +} diff --git a/gen/model_network_status.go b/gen/model_network_status.go new file mode 100644 index 00000000..b8181fea --- /dev/null +++ b/gen/model_network_status.go @@ -0,0 +1,23 @@ +// 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 gen + +// NetworkStatus struct for NetworkStatus +type NetworkStatus struct { + NetworkIdentifier *PartialNetworkIdentifier `json:"network_identifier"` + NetworkInformation *NetworkInformation `json:"network_information"` +} diff --git a/gen/model_network_status_request.go b/gen/model_network_status_request.go new file mode 100644 index 00000000..29e87344 --- /dev/null +++ b/gen/model_network_status_request.go @@ -0,0 +1,22 @@ +// 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 gen + +// NetworkStatusRequest struct for NetworkStatusRequest +type NetworkStatusRequest struct { + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_network_status_response.go b/gen/model_network_status_response.go new file mode 100644 index 00000000..b8966454 --- /dev/null +++ b/gen/model_network_status_response.go @@ -0,0 +1,27 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// NetworkStatusResponse struct for NetworkStatusResponse +type NetworkStatusResponse struct { + NetworkStatus *NetworkStatus `json:"network_status"` + // If a node supports multiple sub-networks, their statuses should be returned in this array. + SubNetworkStatus []*SubNetworkStatus `json:"sub_network_status,omitempty"` + Version *Version `json:"version"` + Options *Options `json:"options"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_operation.go b/gen/model_operation.go new file mode 100644 index 00000000..2f9059a3 --- /dev/null +++ b/gen/model_operation.go @@ -0,0 +1,31 @@ +// Copyright 2020 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated by: OpenAPI Generator (https://openapi-generator.tech) + +package gen + +// 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 Transaction. +type Operation struct { + OperationIdentifier *OperationIdentifier `json:"operation_identifier"` + // Restrict referenced `related_operations` to identifier indexes `<` the current `operation_identifier.index`. This ensures there exists a clear DAG-structure of relations. Since `operations` are one-sided, one could imagine relating operations in a single transfer or linking `operations` in a call tree. + RelatedOperations []*OperationIdentifier `json:"related_operations,omitempty"` + // The network-specific type of the operation. Ensure that any type that can be returned here is also specified in the `NetowrkStatus`. This can be very useful to downstream consumers that parse all block data. + Type string `json:"type"` + // The network-specific status of the operation. Status is not defined on the transaction object because blockchains with smart contracts may have transactions that partially apply. Blockchains with atomic transactions (all operations succeed or all operations fail) will have the same `status` for each operation. + Status string `json:"status"` + Account *AccountIdentifier `json:"account,omitempty"` + Amount *Amount `json:"amount,omitempty"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_operation_identifier.go b/gen/model_operation_identifier.go new file mode 100644 index 00000000..abac69eb --- /dev/null +++ b/gen/model_operation_identifier.go @@ -0,0 +1,25 @@ +// 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 gen + +// OperationIdentifier The `operation_identifier` uniquely identifies an operation within a transaction. +type OperationIdentifier struct { + // The operation `index` is used to ensure each operation has a unique identifier within a transaction. To clarify, there may not be any notion of an operation index in the blockchain being described. + Index int64 `json:"index"` + // Some blockchains specify an operation index that is essential for client use. For example, Bitcoin uses a `network_index` to identify which UTXO was used in a transaction. `network_index` should not be populated if there is no notion of an operation index in a blockchain (typically most account-based blockchains). + NetworkIndex *int64 `json:"network_index,omitempty"` +} diff --git a/gen/model_operation_status.go b/gen/model_operation_status.go new file mode 100644 index 00000000..27698af9 --- /dev/null +++ b/gen/model_operation_status.go @@ -0,0 +1,25 @@ +// 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 gen + +// OperationStatus struct for OperationStatus +type OperationStatus struct { + // The `status` is the network-specific status of the operation. + Status string `json:"status"` + // An `Operation` is considered `successful` if the `Operation.Amount` should affect the `Operation.Account`. Some blockchains (like Bitcoin) only include `successful` operations in blocks but other blockchains (like Ethereum) include unsuccessful operations that incur a fee. To reconcile the computed balance from the stream of `Operations`, it is critical to understand which `Operation.Status` indicate an `Operation` is `successful` and should affect an `Account`. + Successful bool `json:"successful"` +} diff --git a/gen/model_options.go b/gen/model_options.go new file mode 100644 index 00000000..063d6f20 --- /dev/null +++ b/gen/model_options.go @@ -0,0 +1,29 @@ +// 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 gen + +// Options Options specify supported methods, Operation.Status, Operation.Type, and all possible transaction submission statuses. This Options object is used by clients to validate the correctness of a Rosetta Server implementation. It is expected that these clients will error if they receive some response that contains any of the above information that is not specified here. +type Options struct { + // All methods that this implementation supports. + Methods []string `json:"methods"` + // All `Operation.Status` this implementation supports. Any status that is returned during parsing that is not listed here will cause client validation to error. + OperationStatuses []*OperationStatus `json:"operation_statuses"` + // All `Operation.Type` this implementation supports. Any type that is returned during parsing that is not listed here will cause client validation to error. + OperationTypes []string `json:"operation_types"` + // All `status` that can be returned when submitting a transaction using the `/construction/submit` endpoint. + SubmissionStatuses []*SubmissionStatus `json:"submission_statuses"` +} diff --git a/gen/model_partial_block_identifier.go b/gen/model_partial_block_identifier.go new file mode 100644 index 00000000..d5437336 --- /dev/null +++ b/gen/model_partial_block_identifier.go @@ -0,0 +1,23 @@ +// 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 gen + +// 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 request at the current block. +type PartialBlockIdentifier struct { + Index *int64 `json:"index,omitempty"` + Hash *string `json:"hash,omitempty"` +} diff --git a/gen/model_partial_network_identifier.go b/gen/model_partial_network_identifier.go new file mode 100644 index 00000000..ff52035b --- /dev/null +++ b/gen/model_partial_network_identifier.go @@ -0,0 +1,23 @@ +// 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 gen + +// PartialNetworkIdentifier The `partial_network_identifier` specifies which network a particular object is associated with (exculding the `sub_network_identifier`). This identifier is used exclusively in `/network/status`. +type PartialNetworkIdentifier struct { + Blockchain string `json:"blockchain"` + Network string `json:"network"` +} diff --git a/gen/model_peer.go b/gen/model_peer.go new file mode 100644 index 00000000..ccc9692c --- /dev/null +++ b/gen/model_peer.go @@ -0,0 +1,23 @@ +// 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 gen + +// Peer struct for Peer +type Peer struct { + PeerID string `json:"peer_id"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_sub_account_identifier.go b/gen/model_sub_account_identifier.go new file mode 100644 index 00000000..d923d552 --- /dev/null +++ b/gen/model_sub_account_identifier.go @@ -0,0 +1,23 @@ +// 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 gen + +// 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 applicable) an account instantiation refers to. +type SubAccountIdentifier struct { + SubAccount string `json:"sub_account"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_sub_network_identifier.go b/gen/model_sub_network_identifier.go new file mode 100644 index 00000000..878c8336 --- /dev/null +++ b/gen/model_sub_network_identifier.go @@ -0,0 +1,23 @@ +// 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 gen + +// 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 blockchains. +type SubNetworkIdentifier struct { + SubNetwork string `json:"sub_network"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_sub_network_status.go b/gen/model_sub_network_status.go new file mode 100644 index 00000000..68bec694 --- /dev/null +++ b/gen/model_sub_network_status.go @@ -0,0 +1,23 @@ +// 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 gen + +// SubNetworkStatus struct for SubNetworkStatus +type SubNetworkStatus struct { + SubNetworkIdentifier *SubNetworkIdentifier `json:"sub_network_identifier"` + NetworkInformation *NetworkInformation `json:"network_information"` +} diff --git a/gen/model_submission_status.go b/gen/model_submission_status.go new file mode 100644 index 00000000..d1dc0f26 --- /dev/null +++ b/gen/model_submission_status.go @@ -0,0 +1,25 @@ +// 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 gen + +// SubmissionStatus struct for SubmissionStatus +type SubmissionStatus struct { + // The `status` is the network-specific status of transaction submission. + Status string `json:"status"` + // A transaction submission is considered `successful` if there is any way that the transaction could be included in a block. For example, a transaction submission status that indicates a transaction is in the mempool would be `successful` and a status that indicates signature validation failed would not be `successful`. + Successful bool `json:"successful"` +} diff --git a/gen/model_transaction.go b/gen/model_transaction.go new file mode 100644 index 00000000..5d72bb01 --- /dev/null +++ b/gen/model_transaction.go @@ -0,0 +1,25 @@ +// 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 gen + +// Transaction Transactions contain an array of Operations that are attributable to the same TransactionIdentifier. +type Transaction struct { + TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` + Operations []*Operation `json:"operations"` + // Transactions that are related to other transactions (like a cross-shard transactioin) should include the `tranaction_identifier` of these transactions in the metadata. + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_transaction_construction_request.go b/gen/model_transaction_construction_request.go new file mode 100644 index 00000000..d3c772dd --- /dev/null +++ b/gen/model_transaction_construction_request.go @@ -0,0 +1,25 @@ +// 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 gen + +// TransactionConstructionRequest struct for TransactionConstructionRequest +type TransactionConstructionRequest struct { + NetworkIdentifier *NetworkIdentifier `json:"network_identifier"` + AccountIdentifier *AccountIdentifier `json:"account_identifier"` + // Some blockchains require different metadata for different types of transaction construction (ex: delegation versus a transfer). Instead of requiring a blockchain node to return all possible types of metadata for construction (which may require multiple node fetches), the client can specify a `method` to limit the metadata returned to only the subset required. + Method *string `json:"method,omitempty"` +} diff --git a/gen/model_transaction_construction_response.go b/gen/model_transaction_construction_response.go new file mode 100644 index 00000000..effb4dce --- /dev/null +++ b/gen/model_transaction_construction_response.go @@ -0,0 +1,23 @@ +// 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 gen + +// TransactionConstructionResponse struct for TransactionConstructionResponse +type TransactionConstructionResponse struct { + SuggestedFee *Amount `json:"suggested_fee"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_transaction_identifier.go b/gen/model_transaction_identifier.go new file mode 100644 index 00000000..3cbe4ec6 --- /dev/null +++ b/gen/model_transaction_identifier.go @@ -0,0 +1,22 @@ +// 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 gen + +// TransactionIdentifier The `transaction_identifier` uniquely identifies a transaction in a particular network and block or in the mempool. +type TransactionIdentifier struct { + Hash string `json:"hash"` +} diff --git a/gen/model_transaction_submit_request.go b/gen/model_transaction_submit_request.go new file mode 100644 index 00000000..a61c6714 --- /dev/null +++ b/gen/model_transaction_submit_request.go @@ -0,0 +1,22 @@ +// 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 gen + +// TransactionSubmitRequest struct for TransactionSubmitRequest +type TransactionSubmitRequest struct { + SignedTransaction string `json:"signed_transaction"` +} diff --git a/gen/model_transaction_submit_response.go b/gen/model_transaction_submit_response.go new file mode 100644 index 00000000..7e60975b --- /dev/null +++ b/gen/model_transaction_submit_response.go @@ -0,0 +1,25 @@ +// 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 gen + +// TransactionSubmitResponse struct for TransactionSubmitResponse +type TransactionSubmitResponse struct { + TransactionIdentifier *TransactionIdentifier `json:"transaction_identifier"` + // Network-specific transaction submission status + Status string `json:"status"` + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/model_version.go b/gen/model_version.go new file mode 100644 index 00000000..7c045c9c --- /dev/null +++ b/gen/model_version.go @@ -0,0 +1,29 @@ +// 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 gen + +// Version struct for Version +type Version struct { + // The `rosetta_version` is the version of the Rosetta interface the implementation adheres to. This can be useful for clients looking to reliably parse responses. + RosettaVersion string `json:"rosetta_version"` + // The `node_version` is the cannonical version of the node runtime. This can help clients manage deployments. + NodeVersion string `json:"node_version"` + // When a middleware server is used to adhere to the Rosetta interface, it should return its version here. This can help clients manage deployments. + MiddlewareVersion *string `json:"middleware_version,omitempty"` + // Any other information that may be useful about versioning of dependent services should be returned here. + Metadata *map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/gen/response.go b/gen/response.go new file mode 100644 index 00000000..ad99c248 --- /dev/null +++ b/gen/response.go @@ -0,0 +1,53 @@ +// 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 gen + +import ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResonse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..1804b156 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/coinbase/rosetta-sdk-go + +go 1.13 + +require ( + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/stretchr/testify v1.5.1 + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..d7eada07 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/spec.json b/spec.json new file mode 100644 index 00000000..397c1a0b --- /dev/null +++ b/spec.json @@ -0,0 +1,1263 @@ +{ + "openapi": "3.0.2", + "info": { + "version": "1.2.4", + "title": "Rosetta", + "description": "

Backstory

\nWriting reliable blockchain integrations is complicated and time-consuming.\nThe process requires careful analysis of the unique aspects of each blockchain and extensive\ncommunication with its developers to understand the best strategies to deploy nodes,\nrecognize deposits, broadcast transactions, etc. Even a minor misunderstanding can lead to\ndowntime, or even worse, incorrect fund attribution. Not to mention, this integration\nmust be continuously modified and tested each time a blockchain team releases new software.\n\nInstead of spending time working on their blockchain, project developers spend countless hours\nanswering similar support questions for each team integrating their blockchain.\nWith their questions answered, each integrating team then writes similar code to\ninterface with the blockchain instead of spending their engineering resources adding support\nfor more blockchain projects or working on unique products and applications.\n\n

Standard for Blockchain Interaction

\nRosetta is a new project from Coinbase to standardize the process of deploying\nand interacting with blockchains. With an explicit specification to adhere to,\nall parties involved in blockchain development can spend less time figuring out how to integrate\nwith each other and more time working on the novel advances that will push\nthe blockchain ecosystem forward. In practice, this means that any blockchain\nproject that implements the requirements outlined in this specification will enable\nexchanges, block explorers, and wallets to integrate with much less communication\noverhead and network-specific work.\n\n
© 2020 Coinbase
\n", + "x-logo": { + "url": "https://www.centre.io/images/usdc/logos/coinbase-2019-0615383b8a.png" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "http://localhost" + } + ], + "x-tagGroups": [ + { + "name": "Components", + "tags": [ + "Automated Node Deployment", + "RPC Service Interface", + "Future Work" + ] + }, + { + "name": "Schema", + "tags": [ + "Objects", + "Object Identifiers", + "Changelog" + ] + }, + { + "name": "Methods", + "tags": [ + "Network", + "Block", + "Mempool", + "Account", + "Construction" + ] + } + ], + "tags": [ + { + "name": "Automated Node Deployment", + "description": "The first challenge of reliably supporting a blockchain integration is to seamlessly deploy a set of nodes\nto connect to the network. Typically, this includes working with blockchain teams to understand how to\nautomate node deployment, populate configuration files, and set runtime flags. Once configured,\nthis deployment strategy must be maintained across software updates.\n\nIn Rosetta, blockchain teams must create and maintain a single Dockerfile (referencing\nany number of build stages)\nthat starts the node runtime and all of its dependent services without human intervention.\n\n

Requirements

\n

Dockerfile

\nThere are many projects that allow you to configure and manage the automated\ndeployment of a service. From our experience, we have found that the small\nlearning curve, mature tooling, generality, and widespread ecosystem support of\nDocker make for a relatively\npainless and sufficiently reliable deployment.\n\nUpon first glance, using a single Dockerfile to start all services required\nby the node may sound antithetical to best practices. However, we have found\nthat restricting blockchain node deployment to a single container makes\nthe orchestration of multiple nodes much easier because of coordinated start/stop and\nsingle volume mounting.\n\n
Coordinated Start/Stop
\nSome blockchain nodes and their dependent services must be started and stopped in some explicit order\nto function correctly and prevent corruption. With distributed services (in multiple containers),\nthis sequencing of operations can require a custom deployer for each network. Building and maintaining\nthese deployers can take a lot of communication with asset issuers and a lot of testing to ensure\ncorrectness. It is easier to just utilize a script imported into the Dockerfile\nthat can start and stop the node(and its dependent services) correctly.\n\n
Single Volume Mounting
\nWhen a deployment is started from a single Dockerfile it is straightforward to\nmount a single volume to the new container and manage all of its state. Node deployments\ncan be easily scaled by duplicating this volume to any number of new hosts without\nany sophisticated tooling. As mentioned previously, coordinated start/stop of all\nservices provides strong guarantees around the corruption of the state that would\nbe much more difficult to achieve with distributed services as there may be specific\nordering restrictions to prevent corruption.\n\nRunning multiple instances of a node configuration can get complicated quickly if the node\nutilizes multiple stateful containers (ex: a node that stores historical state in an external database).\nIn this scenario, the node orchestration engine must manage which node deployment talks to\nwhich services based on which state the node runtime is in. Furthermore, it is more time-intensive\nto scale up a node deployment as a deployment and all its services must be synced from scratch to ensure correctness\nor the volumes of another deployment's stateful containers must be used to bootstrap the new deployment\n(which can be a manual procedure).\n\n

Persistent Data Storage

\nAll persistent data must be written to the /data directory.\nIf your node utilizes a database (like Postgres) to store data reliably,\nensure this is configured to save information to this directory (and in a manner that would not corrupt\nany data stored by the core node process or other necessary services).\nIt should be possible to stop all services defined in the Dockerfile and\nrestart them (without corruption) using only the state stored in this /data directory\nwith a single command.\n\n

Configurable Storage Pruning

\nIt must be possible to prune persistent data storage using block number. For example,\nanyone running your node should be able to configure the node to delete blocks that are\nover 1000 blocks old. \n" + }, + { + "name": "RPC Service Interface", + "description": "The second challenge of reliably supporting a blockchain integration is\ncorrectly and efficiently communicating with a\ndeployed node. This communication (fetching block data, retrieving account\nstate, submitting transactions, etc) often requires\na myriad of network-specific strategies that must be discussed with a\nblockchain team to ensure correctness. Once complete, these integrations\nrequire constant maintenance to ensure software upgrades do not cause unintended\nbehavior, data irregularities, or degradations in performance.\n\nIn Rosetta, blockchain teams must implement a server that adheres to the\nRosetta Interface (described in the following sections). Blockchain teams\nthat do not wish to modify their node's interface may alternatively\nchoose to write a middleware server that communicates with the node to\nprovide clients with access to the Rosetta Interface. This middleware server\nmay keep persistent state to provide faster query responses (ex: index \nBitcoin UTXOs).\n\n

Design Principles

\nBefore reading this section, it may be helpful to familiarize\nyourself with Rosetta Objects.\n\n

No Predefined Operation Types

\nThe Rosetta interface does not restrict implementations\nto use a predefined set of types to describe network-specific activity.\nFor example, PAYMENT could be a predefined type that could\nbe required for all implementers to apply to network-specific\ntransactions. While this sounds appealing and feasible for a simple type\nlike PAYMENT, the task of producing such a set\nof types can quickly become very complex and controversial. Instead, the\ninterface puts the burden on the client to apply\nwhatever types they feel best apply to the standardized\nOperations that compose each transaction.\n\n

Signed Operation Amounts

\nAlthough there are no predefined Operation types in the\ninterface, an explicit attempt is made to make balance\nreconciliation much easier: using signed Operation amounts.\nThis means that all operations within a transaction\nmust clearly specify the balance change that results from that operation\nare explicitly credit or debits to a specified account.\n\n

Single Party, Linked Operations

\nAll Operations in the interface can affect at most 1 account\nbut can be linked to any number of other Operations.\nTo represent network-specific operations that are inherently\nmulti-participant (a network-specific transfer that has\na sender and recipient specified in a single op), this entails creating an\nexplicit operation for each account involved.\nAt a high-level, this means reducing account-based blockchains to\nsomething resembling ledger accounting (all credits have debits).\nWhile this approach may seem burdensome for simple payments, the\ngenerality of this design allows for capturing the\nstructure of complicated on-chain interactions more closely to how they\nare represented natively on-chain without imposing more restrictions\non the Operation model. This single operation abstraction\nfor all accounting types also means that downstream\nprocessing does not require conditional parsing on different\noperation abstractions.\n\n

Operation Status, Not Transaction Status

\nIndication of status on the Transaction implies\nthat all Operations within that\nTransaction atomically succeed or fail. While this is\noften true on blockchains without smart contracting,\nit is not always the case on blockchains that support generalized smart contracting. In Ethereum,\ntransactions can fail anytime during execution and changes are only rolled back when explicitly\nindicated to do so in the smart contract itself. Requiring that implementers have atomic transaction\nsuccess/failure is a pretty heavy handed restriction that could limit innovation.\n\n

Transaction Fees are Just Operations

\nFor blockchains where there is no explicit fee payer, where there are\nmultiple fee payers, where fees can be paid in multiple\ncurrencies, or where fee payment is made by one of many parties in a\ntransaction, it becomes complicated to represent\na fee payment as a Transaction property. Thus, all\nTransaction fees are represented exclusively as Operations.\n\n

Sharded Blockchain Support

\nMany new blockchains are utilizing a sharded design to offer increased\nscalability. It is critical that this interface treats\nthese blockchains as first-class citizens, albeit in a generalized manner.\nThe interface introduces the idea of SubNetworks\nto identify specific portions of the network that can be used to qualify a\nfetch of blocks or state.\n\n

Staking and Smart Contract Support

\nIn blockchains with generalized smart contracting, the notion of account\nstate can be much more nuanced than a single token balance\nat a single height. The interface introduces SubAccounts to\nidentify state that is specific to a certain contract or lockup period\n(ex: delegated stake). Each account can have an array of balances that\nare uniquely identified by a SubAccount.\n\n

JSON-Based RPC Protocol

\nWhen working on this interface, we considered many interface\nspecification formats that have gained popularity in recent years\n(like Protobuf + gRPC and\nAvro) but found most only\nhad strong support for a few programming languages, required users to\nhave enough programming experience to know how to use autogenerated\ncode to encode/decode some binary format, and/or\ndidn't work well in the browser.\n\nThe Rosetta interface is specified in the OpenAPI 3.0 format (the\nsuccessor to Swagger/OpenAPI 2.0).\nRequests and responses can be crafted with autogenerated code using\nSwagger Codegen or\nOpenAPI Generator (with\nthe downloadable JSON spec at the top of this site),\nare human-readable (easy to debug and understand), and can be used in servers and browsers.\nUsing JSON incurs some performance penalty, however, we have found the challenge of robust\nintegration to be a much more painful issue than mitigating JSON overhead.\n\nDue to the complexity of requests (which can often contain a number of\nparameters of unspecified size), all communication with\nthe Rosetta Interface Server utilizes POST requests. This makes\nthe interface much closer to JSON-RPC 2.0\nwith the exception that requests go to specific paths instead\nof using methods. If you have used gRPC, it will feel very familiar.\n\n

Requirements

\nThese requirements must be satisfied in any Rosetta Interface Server implementation.\n\n

Return All Balance-Changing Operations

\nA Rosetta Interface Server implementation must return all\nbalance-changing operations in a block. To be considered a correct\nimplementaion, the balance computed from all of an\nAccountIdentifier's operations must equal\nthe balance returned for an AccountIdentifier\nby the /account/balance endpoint at the end of each block.\nIf these balances do not match (reconcile)\nfor any account, some clients will not be able to\nintegrate with your blockchain.\n\n

Retrievable AccountIdentifiers

\nAny AccountIdentifier returned in a block must\nbe retrievable from the /account/balance endpoint.\nIf an AccountIdentifier active in a block is not retrievable,\nit will not be possible to compare the balance of the account with the node itself.\n\n

Fully Populated NetworkStatus.Options

\nThe /network/status endpoint returns an Options object that specifies\nall possible Operation.Type, Operation.Status, and transaction submission\nstatuses. This returned Options object is used by clients to validate responses\nfrom your Rosetta Server and will stop processing if any block contains an\nOperation.Type or Operation.Status that is not present here.\n" + }, + { + "name": "Future Work", + "description": "\n" + }, + { + "name": "Objects", + "description": "

Block

\nBlocks contain an array of Transactions that\noccured at a particular BlockIdentifier.\n\n\n

Transaction

\nTransactions contain an array of Operations\nthat are attributable to the same TransactionIdentifier.\n\n\n

Operation

\nOperations contain all balance-changing information within a\ntransaction. They are always one-sided (only affect 1 AccountIdentifier)\nand can succeed or fail independently from a Transaction.\n\n\n

Balance

\nBalance is the array of Amount controlled\nby an AccountIdentifier. An underspecified AccountIdentifier\nmay result in many amounts (ex: all ERC-20 balances for a single address).\n\n\n

Amount

\nAmount is some integer Value of a Currency.\nIt is considered invalid to specify a Value without a Currency.\n\n\n

Currency

\nCurrency is composed of a cannonical Symbol and\nDecimals. This Decimals value is used to convert\nan Amount.Value from atomic units (Satoshis) to standard units\n(Bitcoins).\n\n\n

Options

\nOptions specify supported methods, Operation.Status,\nOperation.Type, and all possible transaction submission statuses. This Options\nobject is used by clients to validate the correctness of a Rosetta Server implementation. It is\nexpected that these clients will error if they receive some response that contains any of the above\ninformation that is not specified here.\n\n" + }, + { + "name": "Object Identifiers", + "description": "

Network Identifier

\nThe network_identifier specifies which network a particular object is associated with.\n\n\n

Sub-Network Identifier

\nIn blockchains with sharded state, the SubNetworkIdentifier\nis required to query some object on a specific shard. This identifier is\noptional for all non-sharded blockchains.\n\n\n

Block Identifier

\nThe BlockIdentifier uniquely identifies a block in a particular network.\n\n\n

Transaction Identifier

\nThe TransactionIdentifier uniquely identifies a transaction\nin a particular network and block or in the mempool.\n\n\n

Operation Identifier

\nThe OperationIdentifier uniquely identifies an operation within a transaction.\n\n\n

Account Identifier

\nThe AccountIdentifier uniquely identifies an account within a network.\nAll fields in the AccountIdentifier are utilized to determine this uniqueness\n(including the metadata field, if populated).\n\n\n

Sub-Account Identifier

\nAn account may have state specific to a contract address (ERC-20 token)\nand/or a stake (delegated balance). The SubAccountIdentifier\nshould specify which state (if applicable) an account instantiation refers\nto.\n\n" + }, + { + "name": "Network", + "description": "Network Methods are used when first connecting to a Rosetta endpoint to determine which network and subnetworks are supported,\nthe current status (the most recent processed block) of the network, and any other useful metadata the server\nwants to provide (ex: version).\n" + }, + { + "name": "Block", + "description": "Block Methods are used to access any data stored in a block. It is critical that these methods can be used to fetch\nall balance-changing operations in a block. If this is not possible, clients that reconcile balances on accounts they\ncare about will not be able to use the implemented interface.\n\nBalance reconciliation, in this scenario, means comparing what balance the node returns for an account\nwith the balance that can be calculate from looking at all the account's transactions.\n" + }, + { + "name": "Mempool", + "description": "Mempool Methods are used to fetch any data stored in the mempool. Note that there are 2 endpoints to fetch transaction\ninformation (transactions in a block and transaction in the mempool).\n" + }, + { + "name": "Account", + "description": "Account Methods are used to fetch the state of an account (identified by a network-specific identifier).\n" + }, + { + "name": "Construction", + "description": "Construction Methods are used to retrieve metadata needed for an account to construct a transaction and used to submit\nsigned transactions. Transaction construction and signing is currently outside the scope of Rosetta.\n" + }, + { + "name": "Changelog", + "description": "

1.2.4

\n
    \n
  • Return Successful Operation Statuses in /network/status: To reconcile (see description in the introduction)\n balance changes, it is necessary to understand which network-specific Operation.Status are result in a balance change.\n If information returned here is incorrect, validation will fail and some clients will be unable to use your implementation.
  • \n
  • Return Transaction Submission Statuses in /network/status: To reliably submit transactions, it is important to understand if\n a network-specific submission status could result in a transaction being included in a block. Being explicit about each status,\n makes automatically testing transaction submission possible.
  • \n
\n

1.2.3

\n
    \n
  • Make \"Version\" Object: Create an object to return node and server version instead of using an\n inline object definition.
  • \n
\n

1.2.2

\n
    \n
  • Modify \"NetworkStatus\" Model for Multiple Sub-Networks Support: In 1.2.1 it was not possible to return\n multiple \"SubNetworkStatus\" in the /network/status method. This prevented sharded blockchains from\n providing sufficient information to clients.
  • \n
  • Add \"network_index\" to OperationIdentifier: Some blockchains have a network_index on\n their operations that is essential for client use. For example, Bitcoin uses a `network_index` to identify\n which UTXO was used in a transaction.
  • \n
\n

1.2.1

\n
    \n
  • Add Timestamp to \"NetworkStatus\": In node orchestration, it is often useful to know the timestamp\n of the current block.
  • \n
  • Return Metadata with Mempool Transaction: Mempool transactions may carry additional metadata\n describing their priority to be included in a block or their descendant transactions (ex: CPFP in Bitcoin).
  • \n
  • Accept Method in Transaction Construction: Different methods may require different metadata for\n construction.
  • \n
\n

1.2.0

\n
    \n
  • Require BlockIdentifier in /block/transaction method: To correctly parse a transaction, it is often necessary to\n reference the block that includes the specified transaction. For example, in Bitcoin it is necessary to know which block\n contains a transaction to determine which miner should receive fee payments. Without specifying a block identifier, the node\n would have to infer which block to use (which could change during a re-org). Albeit more restrictive, the interface now\n requires providing a BlockIdentifier in /block/transaction interactions to eliminate this\n ambiguity. This change is a good opportunity to clarify that this interface is not attempting to force all nodes to \n become block explorers. Rather, this interface is focused on making block data extraction and transaction submission\n standardized. The interface could be extended at some point to define optional \"Explorer Methods\", but this is outside\n the scope of the current specification.
  • \n
  • Remove /account/transactions method: This method often requires additional indexing in the node to answer\n efficiently. This method will be included as a method in \"Explorer Methods\" (outlined in the \"Future Work\" section).\n
  • Change Gateway Methods to Network Methods: Change name of \"Gateway Methods\" to \"Network Methods\" and change\n return type of /network/status to an object instead of an array of networks.
  • \n
\n

1.1.6

\n
    \n
  • Remove \"Cache Integration\" from the \"Future Work\" Section: Originally, there was a plan to allow clients to provide\n prefetched, raw data to a Rosetta Server for parsing instead of requiring the Rosetta Server to interact with a node. \n This would have required the client to know nuanced details about how a Rosetta Server implementation parsed data (to know\n which data to provide) and which versions of raw data were compatible with a specific implementation version.\n Instead, it was decided that any \"cache-like\" enhancements (storing UTXOs for an account) should be handled opaquely\n by the Rosetta Server if certain methods are not performant. Cached data should be stored in the same directory as node data.\n \n
  • Simplify BlockIdentifier: Remove previous_hash from BlockIdentifier and add a \n parent_block_identifier property to objects that depend on knowing the hash of the parent block.
  • \n\n
  • Add PartialBlockIdentifier: In previous versions, requests that included a BlockIdentifier\n (like /block) indicated that it was required to provide the hash, index, and previous_hash.\n This has been relaxed to now be either an index or hash. If neither is provided,\n it is assumed that the client is specifying the current block.
  • \n\n
  • Add Genesis Block Identifier to NetworkStatus: When indexing all blocks, it is often useful to know\n the index and hash of the genesis block.
  • \n
\n" + } + ], + "paths": { + "/network/status": { + "post": { + "summary": "Get Network Status", + "description": "This method returns the current status of the network the node knows about. This method also returns\nthe methods, operation types, and operation statuses the node supports.\n", + "operationId": "networkStatus", + "tags": [ + "Network" + ], + "requestBody": { + "description": "There are not any required fields in this request, yet. It is still specified\nas a `POST` request to ensure required fields can be added without requiring\nclients to change from a `GET`(which is currently more ideal) to a `POST` request.\n", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkStatusRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkStatusResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/block": { + "post": { + "summary": "Get a Block", + "description": "Get a block by its `Block Identifier`\n\nIf transactions are returned in the same call to the node as fetching the block, the response should include these transactions\nin the `Block` object. If not, an array of `Transaction Identifiers` should be returned so `/block/transaction`\nfetches can be done to get all transaction information.\n", + "operationId": "block", + "tags": [ + "Block" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/block/transaction": { + "post": { + "summary": "Get a Block Transaction", + "description": "Get a transaction in a block by its `Transaction Identifier`\n\nThis method should only be used when querying a node for a block does not return all transactions contained within it.\nAll transactions returned by this method must be appended to any transactions returned by the `/block` method by\nconsumers of this data. Fetching a transaction by hash is considered an \"Explorer Method\" (which is classified \nunder the \"Future Work\" section).\n\nCalling this method requires reference to a `BlockIdentifier` because transaction parsing can change depending on which block\ncontains the transaction. For example, in Bitcoin it is necessary to know which block\ncontains a transaction to determine the destination of fee payments. Without specifying a block identifier, the node\nwould have to infer which block to use (which could change during a re-org).\n\nImplementations that require fetching previous transactions to populate the response (ex: Previous UTXOs in Bitcoin) may find it\nuseful to run a cache within the Rosetta server in the `/data` directory (on a path that does not conflict with the node).\n", + "operationId": "blockTransaction", + "tags": [ + "Block" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockTransactionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockTransactionResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/mempool": { + "post": { + "summary": "Get All Mempool Transactions", + "description": "Get all `Transaction Identifiers` in the mempool", + "operationId": "mempool", + "tags": [ + "Mempool" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MempoolRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MempoolResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/mempool/transaction": { + "post": { + "summary": "Get a Mempool Transaction", + "description": "Get a transaction in the mempool by its `Transaction Identifier`.\n\nThis is a separate request than fetching a block transaction (`/block/transaction`) because some blockchain nodes need to\nknow that a transaction query is for something in the mempool instead of a transaction in a block.\n\nTransactions may not be fully parsable until they are in a block (ex: may not be possible to determine the fee to pay\nbefore a transaction is executed). On this endpoint, it is ok that returned transactions are only estimates of what\nmay actually be included in a block.\n", + "operationId": "mempoolTransaction", + "tags": [ + "Mempool" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MempoolTransactionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MempoolTransactionResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/account/balance": { + "post": { + "summary": "Get an Account Balance", + "description": "Get an array of all `Account Balances` for an `Account Identifier` and the `Block Identifier` at which the balance\nlookup was performed.\n\nSome consumers of account balance data need to know at which block the balance was calculated to reconcile account balance changes.\n\nTo get all balances associated with an account, it may be necessary to perform multiple balance requests with unique\n`Account Identifier`s.\n\nIf the client supports it, passing nil `AccountIdentifier` metadata to the request should fetch all balances.\n", + "operationId": "accountBalance", + "tags": [ + "Account" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountBalanceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountBalanceResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/construction/metadata": { + "post": { + "summary": "Get Transaction Construction Metadata", + "description": "Get any information required to construct a transaction for a specific account. Metadata returned here\ncould be a recent hash to use or an account sequence number.\n\nIt is important to clarify that this endpoint should not pre-construct any transactions for the\nclient. All \"account-specific\" metadata must be returned as a key-value mapping so that\ntransaction construction can be audited and performed entirely offline. Any \"account-agnostic\" metadata\ndoes not need to be broken out into a key-value mapping and can be returned as a blob.\n", + "operationId": "transactionConstruction", + "tags": [ + "Construction" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionConstructionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionConstructionResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/construction/submit": { + "post": { + "summary": "Submit a Signed Transaction", + "description": "Submit a signed transaction in network-specific format", + "operationId": "transactionSubmit", + "tags": [ + "Construction" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionSubmitRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionSubmitResponse" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "NetworkIdentifier": { + "description": "The `network_identifier` specifies which network a particular object is associated with.\n", + "type": "object", + "required": [ + "blockchain", + "network" + ], + "properties": { + "blockchain": { + "type": "string", + "example": "bitcoin" + }, + "network": { + "description": "If a blockchain has a specific `chain-id` or network identifier, it\nshould go in this field. It is up to the client to determine which\nnetwork-specific identifier is `mainnet` or `testnet`.\n", + "type": "string", + "example": "mainnet" + }, + "sub_network_identifier": { + "$ref": "#/components/schemas/SubNetworkIdentifier" + } + } + }, + "PartialNetworkIdentifier": { + "description": "The `partial_network_identifier` specifies which network a particular object is associated with\n(exculding the `sub_network_identifier`). This identifier is used exclusively in `/network/status`.\n", + "type": "object", + "required": [ + "blockchain", + "network" + ], + "properties": { + "blockchain": { + "type": "string", + "example": "bitcoin" + }, + "network": { + "type": "string", + "example": "mainnet" + } + } + }, + "SubNetworkIdentifier": { + "description": "In blockchains with sharded state, the SubNetworkIdentifier\nis required to query some object on a specific shard. This identifier is\noptional for all non-sharded blockchains.\n", + "type": "object", + "required": [ + "sub_network" + ], + "properties": { + "sub_network": { + "type": "string", + "example": "shard 1" + }, + "metadata": { + "type": "object", + "example": { + "producer": "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" + } + } + } + }, + "BlockIdentifier": { + "description": "The `block_identifier` uniquely identifies a block in a particular network.\n", + "type": "object", + "required": [ + "index", + "hash" + ], + "properties": { + "index": { + "description": "This is also known as the block height.\n", + "type": "integer", + "format": "int64", + "example": 1123941 + }, + "hash": { + "type": "string", + "example": "0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85" + } + } + }, + "PartialBlockIdentifier": { + "type": "object", + "description": "When fetching data by `BlockIdentifier`, it may be possible to only specify the\n`index` or `hash`. If neither property is specified, it is assumed that the\nclient is making a request at the current block.\n", + "properties": { + "index": { + "type": "integer", + "format": "int64", + "example": 1123941 + }, + "hash": { + "type": "string", + "example": "0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85" + } + } + }, + "TransactionIdentifier": { + "description": "The `transaction_identifier` uniquely identifies a transaction in a particular network and block\nor in the mempool.\n", + "type": "object", + "required": [ + "hash" + ], + "properties": { + "hash": { + "type": "string", + "example": "0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f" + } + } + }, + "OperationIdentifier": { + "description": "The `operation_identifier` uniquely identifies an operation within a transaction.\n", + "type": "object", + "required": [ + "index" + ], + "properties": { + "index": { + "description": "The operation `index` is used to ensure each operation has a unique identifier within\na transaction.\n\nTo clarify, there may not be any notion of an operation index in the blockchain being described.\n", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 1 + }, + "network_index": { + "description": "Some blockchains specify an operation index that is essential for client use. For example,\nBitcoin uses a `network_index` to identify which UTXO was used in a transaction.\n\n`network_index` should not be populated if there is no notion of an operation index in a\nblockchain (typically most account-based blockchains).\n", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 0 + } + } + }, + "AccountIdentifier": { + "description": "The `account_identifier` uniquely identifies an account within a network.\nAll fields in the `account_identifier` are utilized to determine this uniqueness\n(including the `metadata` field, if populated).\n", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "The `address` may be a cryptographic public key (or some encoding of it) or a provided username.\n", + "type": "string" + }, + "sub_account": { + "$ref": "#/components/schemas/SubAccountIdentifier" + }, + "metadata": { + "description": "Blockchains that utilize a username model (where the address is not a derivative of a cryptographic\npublic key) should specify the public key(s) owned by the address in metadata.\n", + "type": "object" + } + } + }, + "SubAccountIdentifier": { + "description": "An account may have state specific to a contract address (ERC-20 token)\nand/or a stake (delegated balance). The `sub_account_identifier` should\nspecify which state (if applicable) an account instantiation refers to.\n", + "type": "object", + "required": [ + "sub_account" + ], + "properties": { + "sub_account": { + "type": "string", + "example": "0x6b175474e89094c44da98b954eedeac495271d0f" + }, + "metadata": { + "type": "object" + } + } + }, + "Block": { + "description": "Blocks contain an array of Transactions that\noccured at a particular BlockIdentifier.\n", + "type": "object", + "required": [ + "block_identifier", + "parent_block_identifier", + "timestamp", + "transactions" + ], + "properties": { + "block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "parent_block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "timestamp": { + "$ref": "#/components/schemas/Timestamp" + }, + "transactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transaction" + } + }, + "metadata": { + "type": "object", + "example": { + "transactions_root": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "difficulty": "123891724987128947" + } + } + } + }, + "Transaction": { + "description": "Transactions contain an array of Operations\nthat are attributable to the same TransactionIdentifier.\n", + "type": "object", + "required": [ + "transaction_identifier", + "operations" + ], + "properties": { + "transaction_identifier": { + "$ref": "#/components/schemas/TransactionIdentifier" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Operation" + } + }, + "metadata": { + "description": "Transactions that are related to other transactions (like a cross-shard transactioin) should include\nthe `tranaction_identifier` of these transactions in the metadata.\n", + "type": "object", + "example": { + "size": 12378, + "lockTime": 1582272577 + } + } + } + }, + "Operation": { + "description": "Operations contain all balance-changing information within a\ntransaction. They are always one-sided (only affect 1 AccountIdentifier)\nand can succeed or fail independently from a Transaction.\n", + "type": "object", + "required": [ + "operation_identifier", + "type", + "status" + ], + "properties": { + "operation_identifier": { + "$ref": "#/components/schemas/OperationIdentifier" + }, + "related_operations": { + "description": "Restrict referenced `related_operations` to identifier indexes\n`<` the current `operation_identifier.index`. This ensures there\nexists a clear DAG-structure of relations.\n\nSince `operations` are one-sided, one could imagine relating operations\nin a single transfer or linking `operations` in a call tree.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/OperationIdentifier" + }, + "example": [ + { + "index": 0, + "operation_identifier": { + "index": 0 + } + } + ] + }, + "type": { + "description": "The network-specific type of the operation. Ensure that any type that can be returned here is also\nspecified in the `NetowrkStatus`. This can be very useful to downstream consumers that parse all\nblock data.\n", + "type": "string", + "example": "Transfer" + }, + "status": { + "description": "The network-specific status of the operation. Status is not defined on the transaction object\nbecause blockchains with smart contracts may have transactions that partially apply.\n\nBlockchains with atomic transactions (all operations succeed or all operations fail) will have\nthe same `status` for each operation.\n", + "type": "string", + "example": "Reverted" + }, + "account": { + "$ref": "#/components/schemas/AccountIdentifier" + }, + "amount": { + "$ref": "#/components/schemas/Amount" + }, + "metadata": { + "type": "object", + "example": { + "asm": "304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2", + "hex": "48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2" + } + } + } + }, + "Amount": { + "description": "Amount is some Value of a Currency.\nIt is considered invalid to specify a Value without a Currency.\n", + "type": "object", + "required": [ + "value", + "currency" + ], + "properties": { + "value": { + "description": "Value of the transaction in atomic units represented as an arbitrary-sized signed integer.\n\nFor example, 1 BTC would be represented by a value of 100000000.\n", + "type": "string", + "example": "-1238089899992" + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "metadata": { + "type": "object" + } + } + }, + "Currency": { + "description": "Currency is composed of a cannonical Symbol and\nDecimals. This Decimals value is used to convert\nan Amount.Value from atomic units (Satoshis) to standard units\n(Bitcoins).\n", + "type": "object", + "required": [ + "symbol", + "decimals" + ], + "properties": { + "symbol": { + "description": "Cannonical symbol associated with a currency.\n", + "type": "string", + "example": "BTC" + }, + "decimals": { + "description": "Number of decimal places in the standard unit representation of the amount.\n\nFor example, BTC has 8 decimals. Note that it is not possible to represent\nthe value of some currency in atomic units that is not base 10.\n", + "type": "integer", + "format": "int32", + "minimum": 0, + "example": 8 + }, + "metadata": { + "description": "Any additional information related to the currency itself.\n\nFor example, it would be useful to populate this object with the contract address\nof an ERC-20 token.\n", + "type": "object", + "example": { + "Issuer": "Satoshi" + } + } + } + }, + "Balance": { + "description": "Balance is the array of Amount controlled\nby an AccountIdentifier. An underspecified AccountIdentifier\nmay result in many amounts (ex: all ERC-20 balances for a single address).\n", + "type": "object", + "required": [ + "account_identifier", + "amounts" + ], + "properties": { + "account_identifier": { + "$ref": "#/components/schemas/AccountIdentifier" + }, + "amounts": { + "description": "A single account may have a balance in multiple currencies.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/Amount" + } + }, + "metadata": { + "description": "Account-based blockchains that utilize a nonce or sequence number\nshould include that number in the metadata. This number could be\nunique to the identifier or global across the account address.\n", + "type": "object", + "example": { + "sequence_number": 23 + } + } + } + }, + "Peer": { + "type": "object", + "required": [ + "peer_id" + ], + "properties": { + "peer_id": { + "type": "string", + "example": "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" + }, + "metadata": { + "type": "object" + } + } + }, + "Version": { + "type": "object", + "required": [ + "rosetta_version", + "node_version" + ], + "properties": { + "rosetta_version": { + "description": "The `rosetta_version` is the version of the Rosetta interface\nthe implementation adheres to. This can be useful for clients\nlooking to reliably parse responses.\n", + "type": "string", + "example": "1.2.4" + }, + "node_version": { + "description": "The `node_version` is the cannonical version of the node\nruntime. This can help clients manage deployments.\n", + "type": "string", + "example": "1.0.2" + }, + "middleware_version": { + "description": "When a middleware server is used to adhere to the Rosetta\ninterface, it should return its version here. This can help\nclients manage deployments.\n", + "type": "string", + "example": "0.2.7" + }, + "metadata": { + "description": "Any other information that may be useful about versioning\nof dependent services should be returned here.\n", + "type": "object" + } + } + }, + "NetworkInformation": { + "type": "object", + "required": [ + "current_block_identifier", + "current_block_timestamp", + "genesis_block_identifier", + "peers" + ], + "properties": { + "current_block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "current_block_timestamp": { + "$ref": "#/components/schemas/Timestamp" + }, + "genesis_block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Peer" + } + } + } + }, + "NetworkStatus": { + "type": "object", + "required": [ + "network_identifier", + "network_information" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/PartialNetworkIdentifier" + }, + "network_information": { + "$ref": "#/components/schemas/NetworkInformation" + } + } + }, + "SubNetworkStatus": { + "type": "object", + "required": [ + "sub_network_identifier", + "network_information" + ], + "properties": { + "sub_network_identifier": { + "$ref": "#/components/schemas/SubNetworkIdentifier" + }, + "network_information": { + "$ref": "#/components/schemas/NetworkInformation" + } + } + }, + "Options": { + "description": "Options specify supported methods, Operation.Status,\nOperation.Type, and all possible transaction submission statuses. This Options\nobject is used by clients to validate the correctness of a Rosetta Server implementation. It is\nexpected that these clients will error if they receive some response that contains any of the above\ninformation that is not specified here.\n", + "type": "object", + "required": [ + "methods", + "operation_statuses", + "operation_types", + "submission_statuses" + ], + "properties": { + "methods": { + "type": "array", + "description": "All methods that this implementation supports.\n", + "items": { + "type": "string", + "example": "/account/transactions" + } + }, + "operation_statuses": { + "description": "All `Operation.Status` this implementation supports. Any status\nthat is returned during parsing that is not listed here will cause\nclient validation to error.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/OperationStatus" + } + }, + "operation_types": { + "description": "All `Operation.Type` this implementation supports. Any type\nthat is returned during parsing that is not listed here will\ncause client validation to error.\n", + "type": "array", + "items": { + "type": "string", + "example": "TRANSFER" + } + }, + "submission_statuses": { + "description": "All `status` that can be returned when submitting a transaction\nusing the `/construction/submit` endpoint.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/SubmissionStatus" + } + } + } + }, + "OperationStatus": { + "type": "object", + "required": [ + "status", + "successful" + ], + "properties": { + "status": { + "description": "The `status` is the network-specific status of the operation.\n", + "type": "string" + }, + "successful": { + "description": "An `Operation` is considered `successful` if the `Operation.Amount`\nshould affect the `Operation.Account`. Some blockchains (like Bitcoin)\nonly include `successful` operations in blocks but other blockchains\n(like Ethereum) include unsuccessful operations that incur a fee.\n\nTo reconcile the computed balance from the stream of `Operations`,\nit is critical to understand which `Operation.Status` indicate an\n`Operation` is `successful` and should affect an `Account`.\n", + "type": "boolean" + } + }, + "example": { + "status": "SUCCESS", + "successful": true + } + }, + "SubmissionStatus": { + "type": "object", + "required": [ + "status", + "successful" + ], + "properties": { + "status": { + "description": "The `status` is the network-specific status of transaction\nsubmission.\n", + "type": "string" + }, + "successful": { + "description": "A transaction submission is considered `successful` if there\nis any way that the transaction could be included in a block.\nFor example, a transaction submission status that indicates\na transaction is in the mempool would be `successful` and a\nstatus that indicates signature validation failed would not\nbe `successful`.\n", + "type": "boolean" + } + }, + "example": { + "status": "MEMPOOL", + "successful": true + } + }, + "Timestamp": { + "description": "The timestamp of the block in milliseconds since the Unix Epoch. The timestamp is stored in\nmilliseconds because some blockchains produce blocks more often than once a second.\n", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 1582833600000 + }, + "BlockRequest": { + "type": "object", + "required": [ + "network_identifier", + "block_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + }, + "block_identifier": { + "$ref": "#/components/schemas/PartialBlockIdentifier" + } + } + }, + "BlockResponse": { + "type": "object", + "required": [ + "block" + ], + "properties": { + "block": { + "$ref": "#/components/schemas/Block" + }, + "other_transactions": { + "description": "Some blockchains may require additional transactions to be fetched that weren't returned in the block response\n(ex: block only returns transaction hashes). For blockchains with a lot of transactions in each block, this\ncan be very useful as consumers can concurrently fetch all transactions returned.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionIdentifier" + } + } + } + }, + "BlockTransactionRequest": { + "type": "object", + "required": [ + "network_identifier", + "block_identifier", + "transaction_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + }, + "block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "transaction_identifier": { + "$ref": "#/components/schemas/TransactionIdentifier" + } + } + }, + "BlockTransactionResponse": { + "type": "object", + "required": [ + "transaction" + ], + "properties": { + "transaction": { + "$ref": "#/components/schemas/Transaction" + } + } + }, + "MempoolRequest": { + "type": "object", + "required": [ + "network_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + } + } + }, + "MempoolResponse": { + "type": "object", + "required": [ + "transaction_identifiers" + ], + "properties": { + "transaction_identifiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionIdentifier" + } + } + } + }, + "MempoolTransactionRequest": { + "type": "object", + "required": [ + "network_identifier", + "transaction_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + }, + "transaction_identifier": { + "$ref": "#/components/schemas/TransactionIdentifier" + } + } + }, + "MempoolTransactionResponse": { + "type": "object", + "required": [ + "transaction" + ], + "properties": { + "transaction": { + "$ref": "#/components/schemas/Transaction" + }, + "metadata": { + "type": "object", + "example": { + "descendant_fees": 123923, + "ancestor_count": 2 + } + } + } + }, + "TransactionConstructionRequest": { + "type": "object", + "required": [ + "network_identifier", + "account_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + }, + "account_identifier": { + "$ref": "#/components/schemas/AccountIdentifier" + }, + "method": { + "description": "Some blockchains require different metadata for different types of transaction\nconstruction (ex: delegation versus a transfer).\n\nInstead of requiring a blockchain node to return all possible types of metadata\nfor construction (which may require multiple node fetches), the client can specify\na `method` to limit the metadata returned to only the subset required.\n", + "type": "string" + } + } + }, + "TransactionConstructionResponse": { + "type": "object", + "required": [ + "suggested_fee" + ], + "properties": { + "suggested_fee": { + "$ref": "#/components/schemas/Amount" + }, + "metadata": { + "type": "object", + "example": { + "account_sequence": 23, + "recent_block_hash": "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" + } + } + } + }, + "TransactionSubmitRequest": { + "type": "object", + "required": [ + "signed_transaction" + ], + "properties": { + "signed_transaction": { + "type": "string" + } + } + }, + "TransactionSubmitResponse": { + "type": "object", + "required": [ + "transaction_identifier", + "status" + ], + "properties": { + "transaction_identifier": { + "$ref": "#/components/schemas/TransactionIdentifier" + }, + "status": { + "description": "Network-specific transaction submission status", + "type": "string", + "example": "memSuccess" + }, + "metadata": { + "type": "object" + } + } + }, + "AccountBalanceRequest": { + "type": "object", + "required": [ + "network_identifier", + "account_identifier" + ], + "properties": { + "network_identifier": { + "$ref": "#/components/schemas/NetworkIdentifier" + }, + "account_identifier": { + "$ref": "#/components/schemas/AccountIdentifier" + } + } + }, + "AccountBalanceResponse": { + "type": "object", + "required": [ + "block_identifier", + "balances" + ], + "properties": { + "block_identifier": { + "$ref": "#/components/schemas/BlockIdentifier" + }, + "balances": { + "description": "A GetAccountBalanceResponse may include multiple uniquely-identified\nbalances. For example, the balance of an account on each shard\ncould be returned or the balance of an account on each ERC-20 contract.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/Balance" + } + } + } + }, + "NetworkStatusRequest": { + "type": "object", + "properties": { + "metadata": { + "type": "object" + } + } + }, + "NetworkStatusResponse": { + "type": "object", + "required": [ + "network_status", + "version", + "options" + ], + "properties": { + "network_status": { + "$ref": "#/components/schemas/NetworkStatus" + }, + "sub_network_status": { + "description": "If a node supports multiple sub-networks, their statuses should\nbe returned in this array.\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/SubNetworkStatus" + } + }, + "version": { + "$ref": "#/components/schemas/Version" + }, + "options": { + "$ref": "#/components/schemas/Options" + }, + "metadata": { + "type": "object", + "example": { + "peer_id": "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" + } + } + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "message": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/templates/README.mustache b/templates/README.mustache new file mode 100644 index 00000000..a2ac1bdb --- /dev/null +++ b/templates/README.mustache @@ -0,0 +1,46 @@ +# rosetta-sdk-go + +[![Coinbase](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master.svg?style=svg)](https://circleci.com/gh/coinbase/rosetta-sdk-go/tree/master) + +## 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}} +- Package version: {{packageVersion}} + +## 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) + +## 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 new file mode 100644 index 00000000..2298132c --- /dev/null +++ b/templates/api.mustache @@ -0,0 +1,221 @@ +{{>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}}{{#summary}} {{{.}}}{{/summary}}{{^summary}} Method for {{operationId}}{{/summary}} +{{#notes}} +{{notes}} +{{/notes}} + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). +{{#allParams}} +{{#required}} + * @param {{paramName}}{{#description}} {{{.}}}{{/description}} +{{/required}} +{{/allParams}} +{{#hasOptionalParams}} + * @param optional nil or *{{#structPrefix}}{{&classname}}{{/structPrefix}}{{{nickname}}}Opts - Optional Parameters: +{{#allParams}} +{{^required}} + * @param "{{vendorExtensions.x-export-param-name}}" ({{#isPrimitiveType}}{{^isBinary}}optional.{{vendorExtensions.x-optional-data-type}}{{/isBinary}}{{#isBinary}}optional.Interface of {{dataType}}{{/isBinary}}{{/isPrimitiveType}}{{^isPrimitiveType}}optional.Interface of {{dataType}}{{/isPrimitiveType}}) - {{#description}} {{{.}}}{{/description}} +{{/required}} +{{/allParams}} +{{/hasOptionalParams}} +{{#returnType}} +@return {{{returnType}}} +{{/returnType}} +*/ +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 ( + localVarHTTPMethod = _nethttp.Method{{httpMethod}} + 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, localVarHTTPMethod, 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 != 200 { + 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.mustache b/templates/client.mustache new file mode 100644 index 00000000..dbe3e48c --- /dev/null +++ b/templates/client.mustache @@ -0,0 +1,356 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "reflect" + "regexp" + "strings" + "time" +) + +const ( + // APIVersion is the version of the Rosetta API Spec + // used to generate this code. + APIVersion = "{{appVersion}}" +) + +var ( + jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) +) + +// APIClient manages communication with the {{appName}} API v{{version}} +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services +{{#apiInfo}} +{{#apis}} +{{#operations}} + + {{classname}} *{{classname}}Service +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + c.common.client = c + +{{#apiInfo}} + // API Services +{{#apis}} +{{#operations}} + c.{{classname}} = (*{{classname}}Service)(&c.common) +{{/operations}} +{{/apis}} +{{/apiInfo}} + + return c +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insenstive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.ToLower(a) == strings.ToLower(needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +// parameterToString convert interface{} parameters to string, using a delimiter if format is provided. +func parameterToString(obj interface{}, collectionFormat string) string { + var delimiter string + + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") + } else if t, ok := obj.(time.Time); ok { + return t.Format(time.RFC3339) + } + + return fmt.Sprintf("%v", obj) +} + +// helper for converting interface{} parameters to json strings +func parameterToJson(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + + +// callAPI do the request. +func (c *APIClient) callAPI(ctx context.Context, request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request.WithContext(ctx)) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + + return resp, err +} + +// ChangeBasePath changes base path to allow switching to mocks +func (c *APIClient) ChangeBasePath(path string) { + c.cfg.BasePath = path +} + +// GetConfig allows for modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, +) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Generate a new request + localVarRequest, err = http.NewRequest(method, url.String(), body) + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers.Set(h, v) + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } + + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if jsonCheck.MatchString(contentType) { + if err = json.Unmarshal(b, v); err != nil { + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if jsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("invalid body type %s", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + 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/configuration.mustache new file mode 100644 index 00000000..a62e5748 --- /dev/null +++ b/templates/configuration.mustache @@ -0,0 +1,166 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request. + ContextOAuth2 = contextKey("token") + + // ContextBasicAuth takes BasicAuth as authentication for the request. + ContextBasicAuth = contextKey("basic") + + // ContextAccessToken takes a string oauth2 access token as authentication for the request. + ContextAccessToken = contextKey("accesstoken") + + // ContextAPIKey takes an APIKey as authentication for the request + ContextAPIKey = contextKey("apikey") + + {{#withAWSV4Signature}} + // ContextAWSv4 takes an Access Key and a Secret Key for signing AWS Signature v4. + ContextAWSv4 = contextKey("awsv4") + {{/withAWSV4Signature}} +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +{{#withAWSV4Signature}} +// AWSv4 provides AWS Signature to a request passed via context using ContextAWSv4 +// https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html +type AWSv4 struct { + AccessKey string + SecretKey string +} +{{/withAWSV4Signature}} + +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + Url string + Description string + Variables map[string]ServerVariable +} + +// Configuration stores the configuration of the API client +type Configuration struct { + BasePath string `json:"basePath,omitempty"` + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers []ServerConfiguration + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration(basePath string, userAgent string, httpClient *http.Client) *Configuration { + cfg := &Configuration{ + BasePath: basePath, + DefaultHeader: make(map[string]string), + UserAgent: userAgent, + Debug: false, + {{#servers}} + {{#-first}} + Servers: []ServerConfiguration{ + {{/-first}} + { + Url: basePath, + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + Variables: map[string]ServerVariable{ + {{/-first}} + "{{{name}}}": ServerVariable{ + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + DefaultValue: "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + EnumValues: []string{ + {{/-first}} + "{{{.}}}", + {{#-last}} + }, + {{/-last}} + {{/enumValues}} + }, + {{#-last}} + }, + {{/-last}} + {{/variables}} + }, + {{#-last}} + }, + {{/-last}} + {{/servers}} + } + + if httpClient != nil { + cfg.HTTPClient = httpClient + } + + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// ServerUrl returns URL based on server settings +func (c *Configuration) ServerUrl(index int, variables map[string]string) (string, error) { + if index < 0 || len(c.Servers) <= index { + return "", fmt.Errorf("Index %v out of range %v", index, len(c.Servers) - 1) + } + server := c.Servers[index] + url := server.Url + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("The variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} diff --git a/templates/model.mustache b/templates/model.mustache new file mode 100644 index 00000000..c972f58f --- /dev/null +++ b/templates/model.mustache @@ -0,0 +1,43 @@ +{{>partial_header}} +package {{packageName}} +{{#models}} +{{#imports}} +{{#-first}} +import ( +{{/-first}} + "{{import}}" +{{#-last}} +) +{{/-last}} +{{/imports}} +{{#model}} +{{#isEnum}} +// {{{classname}}} {{#description}}{{{.}}}{{/description}}{{^description}}the model '{{{classname}}}'{{/description}} +type {{{classname}}} {{^format}}{{dataType}}{{/format}}{{#format}}{{{format}}}{{/format}} + +// List of {{{name}}} +const ( + {{#allowableValues}} + {{#enumVars}} + {{^-first}} + {{/-first}} + {{#enumClassPrefix}}{{{classname.toUpperCase}}}_{{/enumClassPrefix}}{{name}} {{{classname}}} = {{{value}}} + {{/enumVars}} + {{/allowableValues}} +) +{{/isEnum}} +{{^isEnum}} +// {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}} +type {{classname}} struct { +{{#allVars}} +{{^-first}} +{{/-first}} +{{#description}} + // {{{description}}} +{{/description}} + {{name}} {{^isPrimitiveType}}{{#required}}*{{/required}}{{/isPrimitiveType}}{{^required}}*{{/required}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"` +{{/allVars}} +} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/templates/partial_header.mustache b/templates/partial_header.mustache new file mode 100644 index 00000000..a0a1639a --- /dev/null +++ b/templates/partial_header.mustache @@ -0,0 +1 @@ +// Generated by: OpenAPI Generator (https://openapi-generator.tech) diff --git a/templates/response.mustache b/templates/response.mustache new file mode 100644 index 00000000..1a8765ba --- /dev/null +++ b/templates/response.mustache @@ -0,0 +1,38 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResonse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +}