diff --git a/cmd/check_spec.go b/cmd/check_spec.go index 9a8b1aea..fbd04983 100644 --- a/cmd/check_spec.go +++ b/cmd/check_spec.go @@ -21,6 +21,7 @@ import ( "github.com/coinbase/rosetta-cli/pkg/results" "github.com/coinbase/rosetta-sdk-go/fetcher" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/spf13/cobra" ) @@ -35,35 +36,16 @@ var ( } ) -// checkSpec struct should implement following interface -// type checkSpecer interface { -// NetworkList() error -// NetworkOptions() error -// NetworkStatus(ctx context.Context) error - -// AccountBalance() error -// AccountCoins() error - -// Block() error -// BlockTransaction() error - -// ConstructionCombine() error -// ConstructionHash() error -// ConstructionMetadata() error -// ConstructionParse() error -// ConstructionPayloads() error -// ConstructionPreprocess() error -// ConstructionSubmit() error - -// Error() error -// MultipleModes() error -// } type checkSpec struct { onlineFetcher *fetcher.Fetcher offlineFetcher *fetcher.Fetcher } func newCheckSpec(ctx context.Context) (*checkSpec, error) { + if Config.Construction == nil { + return nil, fmt.Errorf("%v", errRosettaConfigNoConstruction) + } + onlineFetcherOpts := []fetcher.Option{ fetcher.WithMaxConnections(Config.MaxOnlineConnections), fetcher.WithRetryElapsedTime(time.Duration(Config.RetryElapsedTime) * time.Second), @@ -98,19 +80,7 @@ func newCheckSpec(ctx context.Context) (*checkSpec, error) { Config, nil, nil, - fmt.Errorf("%w: unable to initialize asserter for online node fetcher", fetchErr.Err), - "", - "", - ) - } - - _, _, fetchErr = offlineFetcher.InitializeAsserter(ctx, Config.Network, Config.ValidationFile) - if fetchErr != nil { - return nil, results.ExitData( - Config, - nil, - nil, - fmt.Errorf("%w: unable to initialize asserter for offline node fetcher", fetchErr.Err), + fmt.Errorf("%v: unable to initialize asserter for online node fetcher", fetchErr.Err), "", "", ) @@ -122,58 +92,442 @@ func newCheckSpec(ctx context.Context) (*checkSpec, error) { }, nil } -func (cs *checkSpec) NetworkStatus(ctx context.Context) error { +func (cs *checkSpec) networkOptions(ctx context.Context) checkSpecOutput { + printInfo("validating /network/options ...\n") + output := checkSpecOutput{ + api: networkOptions, + validation: map[checkSpecRequirement]checkSpecStatus{ + version: checkSpecSuccess, + allow: checkSpecSuccess, + offlineMode: checkSpecSuccess, + }, + } + defer printInfo("/network/options validated\n") + + res, err := cs.offlineFetcher.NetworkOptionsRetry(ctx, Config.Network, nil) + if err != nil { + printError("%v: unable to fetch network options\n", err.Err) + markAllValidationsFailed(output) + return output + } + + // version is required + if res.Version == nil { + setValidationStatus(output, version, checkSpecFailure) + printError("%v: unable to find version in /network/options response\n", errVersionNullPointer) + } + + if err := validateVersion(res.Version.RosettaVersion); err != nil { + setValidationStatus(output, version, checkSpecFailure) + printError("%v\n", err) + } + + if err := validateVersion(res.Version.NodeVersion); err != nil { + setValidationStatus(output, version, checkSpecFailure) + printError("%v\n", err) + } + + // allow is required + if res.Allow == nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v: unable to find allow in /network/options response\n", errAllowNullPointer) + } + + if err := validateOperationStatuses(res.Allow.OperationStatuses); err != nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v\n", err) + } + + if err := validateOperationTypes(res.Allow.OperationTypes); err != nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v\n", err) + } + + if err := validateErrors(res.Allow.Errors); err != nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v\n", err) + } + + if err := validateCallMethods(res.Allow.CallMethods); err != nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v\n", err) + } + + if err := validateBalanceExemptions(res.Allow.BalanceExemptions); err != nil { + setValidationStatus(output, allow, checkSpecFailure) + printError("%v\n", err) + } + + return output +} + +func (cs *checkSpec) networkStatus(ctx context.Context) checkSpecOutput { + printInfo("validating /network/status ...\n") + output := checkSpecOutput{ + api: networkStatus, + validation: map[checkSpecRequirement]checkSpecStatus{ + currentBlockID: checkSpecSuccess, + currentBlockTime: checkSpecSuccess, + genesisBlockID: checkSpecSuccess, + }, + } + defer printInfo("/network/status validated\n") + res, err := cs.onlineFetcher.NetworkStatusRetry(ctx, Config.Network, nil) if err != nil { - return fmt.Errorf("%w: unable to fetch network status", err.Err) + printError("%v: unable to fetch network status\n", err.Err) + markAllValidationsFailed(output) + return output } - if err := verifyBlockIdentifier(res.CurrentBlockIdentifier); err != nil { - return fmt.Errorf("%w", err) + // current_block_identifier is required + if err := validateBlockIdentifier(res.CurrentBlockIdentifier); err != nil { + printError("%v\n", err) + setValidationStatus(output, currentBlockID, checkSpecFailure) } - // TODO - return nil + // current_block_timestamp is required + if err := validateTimestamp(res.CurrentBlockTimestamp); err != nil { + printError("%v\n", err) + setValidationStatus(output, currentBlockTime, checkSpecFailure) + } + + // genesis_block_identifier is required + if err := validateBlockIdentifier(res.GenesisBlockIdentifier); err != nil { + printError("%v\n", err) + setValidationStatus(output, genesisBlockID, checkSpecFailure) + } + + return output } -func (cs *checkSpec) NetworkList(ctx context.Context, fetcher *fetcher.Fetcher) error { - networks, err := fetcher.NetworkList(ctx, nil) +func (cs *checkSpec) networkList(ctx context.Context) checkSpecOutput { + printInfo("validating /network/list ...\n") + output := checkSpecOutput{ + api: networkList, + validation: map[checkSpecRequirement]checkSpecStatus{ + networkIDs: checkSpecSuccess, + offlineMode: checkSpecSuccess, + staticNetworkID: checkSpecSuccess, + }, + } + defer printInfo("/network/list validated\n") + + networks, err := cs.offlineFetcher.NetworkList(ctx, nil) if err != nil { - return fmt.Errorf("%w: unable to fetch network list", err.Err) + printError("%v: unable to fetch network list", err.Err) + markAllValidationsFailed(output) + return output } + if len(networks.NetworkIdentifiers) == 0 { - return fmt.Errorf("network_identifiers are required") + printError("network_identifiers is required") + setValidationStatus(output, networkIDs, checkSpecFailure) } + for _, network := range networks.NetworkIdentifiers { - if network.Network == Config.Network.Network && - network.Blockchain == Config.Network.Blockchain { - return nil + if isEqual(network.Network, Config.Network.Network) && + isEqual(network.Blockchain, Config.Network.Blockchain) { + return output } } - return fmt.Errorf("network identifier in configuration file is not returned by /network/list") + + printError("network_identifier in configuration file is not returned by /network/list") + setValidationStatus(output, staticNetworkID, checkSpecFailure) + return output } -func runCheckSpecCmd(_ *cobra.Command, _ []string) error { - ctx := context.Background() - cs, err := newCheckSpec(ctx) +func (cs *checkSpec) accountBalance(ctx context.Context) checkSpecOutput { + printInfo("validating /account/balance ...\n") + output := checkSpecOutput{ + api: accountBalance, + validation: map[checkSpecRequirement]checkSpecStatus{ + blockID: checkSpecSuccess, + balances: checkSpecSuccess, + }, + } + defer printInfo("/account/balance validated\n") + + acct, partBlockID, currencies, err := cs.getAccount(ctx) + if err != nil { + markAllValidationsFailed(output) + printError("%v: unable to get an account\n", err) + return output + } + if acct == nil { + markAllValidationsFailed(output) + printError("%v\n", errAccountNullPointer) + return output + } + + // fetch account balance + block, amt, _, fetchErr := cs.onlineFetcher.AccountBalanceRetry( + ctx, + Config.Network, + acct, + partBlockID, + currencies) if err != nil { - return fmt.Errorf("%w: unable to create checkSpec object", err) + markAllValidationsFailed(output) + printError("%v: unable to fetch balance for account: %v\n", fetchErr.Err, *acct) + return output + } + + // block_identifier is required + if err := validateBlockIdentifier(block); err != nil { + printError("%v\n", err) + setValidationStatus(output, blockID, checkSpecFailure) + } + + // balances is required + if err := validateBalances(amt); err != nil { + printError("%v\n", err) + setValidationStatus(output, balances, checkSpecFailure) + } + + return output +} + +func (cs *checkSpec) accountCoins(ctx context.Context) checkSpecOutput { + printInfo("validating /account/coins ...\n") + output := checkSpecOutput{ + api: accountCoins, + validation: map[checkSpecRequirement]checkSpecStatus{ + blockID: checkSpecSuccess, + coins: checkSpecSuccess, + }, + } + defer printInfo("/account/coins validated\n") + + if isUTXO() { + acct, _, currencies, err := cs.getAccount(ctx) + if err != nil { + printError("%v: unable to get an account\n", err) + markAllValidationsFailed(output) + return output + } + if err != nil { + printError("%v\n", errAccountNullPointer) + markAllValidationsFailed(output) + return output + } + + block, cs, _, fetchErr := cs.onlineFetcher.AccountCoinsRetry( + ctx, + Config.Network, + acct, + false, + currencies) + if fetchErr != nil { + printError("%v: unable to get coins for account: %v\n", fetchErr.Err, *acct) + markAllValidationsFailed(output) + return output + } + + // block_identifier is required + err = validateBlockIdentifier(block) + if err != nil { + printError("%v\n", err) + setValidationStatus(output, blockID, checkSpecFailure) + } + + // coins is required + err = validateCoins(cs) + if err != nil { + printError("%v\n", err) + setValidationStatus(output, coins, checkSpecFailure) + } + } + + return output +} + +func (cs *checkSpec) block(ctx context.Context) checkSpecOutput { + printInfo("validating /block ...\n") + output := checkSpecOutput{ + api: block, + validation: map[checkSpecRequirement]checkSpecStatus{ + idempotent: checkSpecSuccess, + defaultTip: checkSpecSuccess, + }, + } + defer printInfo("/block validated\n") + + res, fetchErr := cs.onlineFetcher.NetworkStatusRetry(ctx, Config.Network, nil) + if fetchErr != nil { + printError("%v: unable to get network status\n", fetchErr.Err) + markAllValidationsFailed(output) + return output + } + + // multiple calls with the same hash should return the same block + var block *types.Block + tip := res.CurrentBlockIdentifier + callTimes := 3 + + for i := 0; i < callTimes; i++ { + blockID := types.PartialBlockIdentifier{ + Hash: &tip.Hash, + } + b, fetchErr := cs.onlineFetcher.BlockRetry(ctx, Config.Network, &blockID) + if fetchErr != nil { + printError("%v: unable to fetch block %v\n", fetchErr.Err, blockID) + markAllValidationsFailed(output) + return output + } + + if block == nil { + block = b + } else if !isEqual(types.Hash(*block), types.Hash(*b)) { + printError("%v\n", errBlockNotIdempotent) + setValidationStatus(output, idempotent, checkSpecFailure) + } + } + + // fetch the tip block again + res, fetchErr = cs.onlineFetcher.NetworkStatusRetry(ctx, Config.Network, nil) + if fetchErr != nil { + printError("%v: unable to get network status\n", fetchErr.Err) + setValidationStatus(output, defaultTip, checkSpecFailure) + return output + } + tip = res.CurrentBlockIdentifier + + // tip shoud be returned if block_identifier is not specified + emptyBlockID := &types.PartialBlockIdentifier{} + block, fetchErr = cs.onlineFetcher.BlockRetry(ctx, Config.Network, emptyBlockID) + if fetchErr != nil { + printError("%v: unable to fetch tip block\n", fetchErr.Err) + setValidationStatus(output, defaultTip, checkSpecFailure) + return output } - if err = cs.NetworkStatus(ctx); err != nil { - return fmt.Errorf("%w: network status verification failed", err) + // block index returned from /block should be >= the index returned by /network/status + if isNegative(block.BlockIdentifier.Index - tip.Index) { + printError("%v\n", errBlockTip) + setValidationStatus(output, defaultTip, checkSpecFailure) } - if err = cs.NetworkList(ctx, cs.onlineFetcher); err != nil { - return fmt.Errorf("%w: online network list verification failed", err) + return output +} + +func (cs *checkSpec) errorObject(ctx context.Context) checkSpecOutput { + printInfo("validating error object ...\n") + output := checkSpecOutput{ + api: errorObject, + validation: map[checkSpecRequirement]checkSpecStatus{ + errorCode: checkSpecSuccess, + errorMessage: checkSpecSuccess, + }, + } + defer printInfo("error object validated\n") + + printInfo("%v\n", "sending request to /network/status ...") + emptyNetwork := &types.NetworkIdentifier{} + _, err := cs.onlineFetcher.NetworkStatusRetry(ctx, emptyNetwork, nil) + validateErrorObject(err, output) + + printInfo("%v\n", "sending request to /network/options ...") + _, err = cs.onlineFetcher.NetworkOptionsRetry(ctx, emptyNetwork, nil) + validateErrorObject(err, output) + + printInfo("%v\n", "sending request to /account/balance ...") + emptyAcct := &types.AccountIdentifier{} + emptyPartBlock := &types.PartialBlockIdentifier{} + emptyCur := []*types.Currency{} + _, _, _, err = cs.onlineFetcher.AccountBalanceRetry(ctx, emptyNetwork, emptyAcct, emptyPartBlock, emptyCur) + validateErrorObject(err, output) + + if isUTXO() { + printInfo("%v\n", "sending request to /account/coins ...") + _, _, _, err = cs.onlineFetcher.AccountCoinsRetry(ctx, emptyNetwork, emptyAcct, false, emptyCur) + validateErrorObject(err, output) + } else { + printInfo("%v\n", "skip /account/coins for account based chain") + } + + printInfo("%v\n", "sending request to /block ...") + _, err = cs.onlineFetcher.BlockRetry(ctx, emptyNetwork, emptyPartBlock) + validateErrorObject(err, output) + + printInfo("%v\n", "sending request to /block/transaction ...") + emptyTx := []*types.TransactionIdentifier{} + emptyBlock := &types.BlockIdentifier{} + _, err = cs.onlineFetcher.UnsafeTransactions(ctx, emptyNetwork, emptyBlock, emptyTx) + validateErrorObject(err, output) + + return output +} + +// Searching for an account backwards from the tip +func (cs *checkSpec) getAccount(ctx context.Context) ( + *types.AccountIdentifier, + *types.PartialBlockIdentifier, + []*types.Currency, + error) { + res, err := cs.onlineFetcher.NetworkStatusRetry(ctx, Config.Network, nil) + if err != nil { + return nil, nil, nil, fmt.Errorf("%v: unable to get network status", err.Err) + } + + var acct *types.AccountIdentifier + var blockID *types.PartialBlockIdentifier + tip := res.CurrentBlockIdentifier.Index + genesis := res.GenesisBlockIdentifier.Index + currencies := []*types.Currency{} + + for i := tip; i >= genesis && acct == nil; i-- { + blockID = &types.PartialBlockIdentifier{ + Index: &i, + } + + block, err := cs.onlineFetcher.BlockRetry(ctx, Config.Network, blockID) + if err != nil { + return nil, nil, nil, fmt.Errorf("%v: unable to fetch block at index: %v", err.Err, i) + } + + // looking for an account in block transactions + for _, tx := range block.Transactions { + for _, op := range tx.Operations { + if op.Account != nil && op.Amount.Currency != nil { + acct = op.Account + currencies = append(currencies, op.Amount.Currency) + break + } + } + + if acct != nil { + break + } + } } - if err = cs.NetworkList(ctx, cs.offlineFetcher); err != nil { - return fmt.Errorf("%w: offline network list verification failed", err) + return acct, blockID, currencies, nil +} + +func runCheckSpecCmd(_ *cobra.Command, _ []string) error { + ctx := context.Background() + cs, err := newCheckSpec(ctx) + if err != nil { + return fmt.Errorf("%v: unable to create checkSpec object with online URL", err) } - // TODO: more checks + output := []checkSpecOutput{} + // validate api endpoints + output = append(output, cs.networkStatus(ctx)) + output = append(output, cs.networkList(ctx)) + output = append(output, cs.networkOptions(ctx)) + output = append(output, cs.accountBalance(ctx)) + output = append(output, cs.accountCoins(ctx)) + output = append(output, cs.block(ctx)) + output = append(output, cs.errorObject(ctx)) + output = append(output, twoModes()) + + printInfo("check:spec is complete\n") + printCheckSpecOutputHeader() + for _, o := range output { + printCheckSpecOutputBody(o) + } - fmt.Println("Successfully validated check:spec") return nil } diff --git a/cmd/check_spec_utils.go b/cmd/check_spec_utils.go index 0e8f4a63..52d9e4d9 100644 --- a/cmd/check_spec_utils.go +++ b/cmd/check_spec_utils.go @@ -16,18 +16,333 @@ package cmd import ( "errors" + "fmt" + "github.com/coinbase/rosetta-sdk-go/fetcher" "github.com/coinbase/rosetta-sdk-go/types" + "github.com/fatih/color" ) var ( - errBlockIdentifier = errors.New("BlockIdentifier must have both index and hash") + errBlockIdentifierNullPointer = errors.New("Null pointer to BlockIdentifier object") + errBlockIdentifierEmptyHash = errors.New("BlockIdentifier can't have empty hash") + errBlockIdentifierNegativeIndex = errors.New("BlockIdentifier can't have negative index") + + errTimestampNegative = errors.New("Timestamp can't be negative") + errVersionEmpty = errors.New("Version can't be empty") + errVersionNullPointer = errors.New("Null pointer to Version object") + + errOperationStatusEmptyStatus = errors.New("OperationStatus can't have empty status value") + errOperationStatusNullPointer = errors.New("Null pointer to OperationStatus object") + errOperationTypeEmpty = errors.New("OperationType can't be empty") + + errErrorEmpty = errors.New("Error object can't be empty") + errErrorEmptyMessage = errors.New("Error object can't have empty message") + errErrorNegativeCode = errors.New("Error object can't have negative code") + + errCallMethodEmptyName = errors.New("Call method name can't be empty") + errBalanceExemptionEmpty = errors.New("Balance exemption can't be empty") + errAllowNullPointer = errors.New("Null pointer to Allow object") + + errBalanceEmptyValue = errors.New("Amount can't be empty") + errCurrencyEmptySymbol = errors.New("Currency can't have empty symbol") + errCurrencyNegativeDecimals = errors.New("Currency can't have negative decimals") + errAccountNullPointer = errors.New("Null pointer to Account object") + + errCoinIdentifierEmpty = errors.New("Coin identifier can't be empty") + errCoinIdentifierNullPointer = errors.New("Null pointer to coin identifier object") + errBlockNotIdempotent = errors.New("Multiple calls with the same hash don't return the same block") + errBlockTip = errors.New("Unspecified block_identifier doesn't give the tip block") + errRosettaConfigNoConstruction = errors.New("No construction element in Rosetta config") +) + +type checkSpecAPI string +type checkSpecRequirement string +type checkSpecStatus string + +const ( + networkList checkSpecAPI = "/network/list" + networkOptions checkSpecAPI = "/network/options" + networkStatus checkSpecAPI = "/network/status" + accountBalance checkSpecAPI = "/account/balance" + accountCoins checkSpecAPI = "/account/coins" + block checkSpecAPI = "/block" + errorObject checkSpecAPI = "error object" + modes checkSpecAPI = "modes" + + networkIDs checkSpecRequirement = "network_identifiers is required" + offlineMode checkSpecRequirement = "endpoint should work in offline mode" + staticNetworkID checkSpecRequirement = "network_identifier must be static" + version checkSpecRequirement = "field version is required" + allow checkSpecRequirement = "field allow is required" + + currentBlockID checkSpecRequirement = "current_block_identifier is required" + currentBlockTime checkSpecRequirement = "current_block_timestamp is required" + genesisBlockID checkSpecRequirement = "genesis_block_identifier is required" + + blockID checkSpecRequirement = "block_identifier is required" + balances checkSpecRequirement = "field balances is required" + coins checkSpecRequirement = "field coins is required" + idempotent checkSpecRequirement = "same hash should return the same block" + defaultTip checkSpecRequirement = "tip should be returned if block_identifier is not specified" + + errorCode checkSpecRequirement = "error code is required" + errorMessage checkSpecRequirement = "error message is required" + diffURLs checkSpecRequirement = "offline_url should be different from offline_url and not empty" + + checkSpecSuccess checkSpecStatus = "Success" + checkSpecFailure checkSpecStatus = "Failure" ) -func verifyBlockIdentifier(blockID *types.BlockIdentifier) error { - if blockID != nil && blockID.Hash != "" && blockID.Index >= 0 { - return nil +type checkSpecOutput struct { + api checkSpecAPI + validation map[checkSpecRequirement]checkSpecStatus +} + +func validateBlockIdentifier(blockID *types.BlockIdentifier) error { + if blockID == nil { + return errBlockIdentifierNullPointer + } + + if isEmpty(blockID.Hash) { + return errBlockIdentifierEmptyHash + } + + if isNegative(blockID.Index) { + return errBlockIdentifierNegativeIndex + } + + return nil +} + +func validateTimestamp(time int64) error { + if isNegative(time) { + return errTimestampNegative + } + + return nil +} + +func validateVersion(version string) error { + if isEmpty(version) { + return errVersionEmpty + } + + return nil +} + +func validateOperationStatuses(oss []*types.OperationStatus) error { + for _, os := range oss { + if os == nil { + return errOperationStatusNullPointer + } + + if isEmpty(os.Status) { + return errOperationStatusEmptyStatus + } + } + + return nil +} + +func validateOperationTypes(ots []string) error { + for _, ot := range ots { + if isEmpty(ot) { + return errOperationTypeEmpty + } + } + + return nil +} + +func validateErrors(errors []*types.Error) error { + for _, err := range errors { + if err == nil { + return errErrorEmpty + } + + if isNegative(int64(err.Code)) { + return errErrorNegativeCode + } + + if isEmpty(err.Message) { + return errErrorEmptyMessage + } + } + + return nil +} + +func validateCallMethods(methods []string) error { + for _, m := range methods { + if isEmpty(m) { + return errCallMethodEmptyName + } + } + + return nil +} + +func validateBalanceExemptions(exs []*types.BalanceExemption) error { + for _, e := range exs { + if e == nil { + return errBalanceExemptionEmpty + } + } + + return nil +} + +func validateBalances(balances []*types.Amount) error { + for _, b := range balances { + if err := validateBalance(b); err != nil { + return err + } } - return errBlockIdentifier + return nil +} + +func validateBalance(amt *types.Amount) error { + if isEmpty(amt.Value) { + return errBalanceEmptyValue + } + + if err := validateCurrency(amt.Currency); err != nil { + return err + } + + return nil +} + +func validateCurrency(currency *types.Currency) error { + if isEmpty(currency.Symbol) { + return errCurrencyEmptySymbol + } + + if isNegative(int64(currency.Decimals)) { + return errCurrencyNegativeDecimals + } + + return nil +} + +func validateCoins(coins []*types.Coin) error { + for _, coin := range coins { + if err := validateCoinIdentifier(coin.CoinIdentifier); err != nil { + return err + } + if err := validateBalance(coin.Amount); err != nil { + return err + } + } + + return nil +} + +func validateCoinIdentifier(coinID *types.CoinIdentifier) error { + if coinID == nil { + return errCoinIdentifierNullPointer + } + + if isEmpty(coinID.Identifier) { + return errCoinIdentifierEmpty + } + + return nil +} + +func twoModes() checkSpecOutput { + output := checkSpecOutput{ + api: modes, + validation: map[checkSpecRequirement]checkSpecStatus{ + diffURLs: checkSpecSuccess, + }, + } + + if isEmpty(Config.OnlineURL) || + isEmpty(Config.Construction.OfflineURL) || + isEqual(Config.OnlineURL, Config.Construction.OfflineURL) { + setValidationStatus(output, diffURLs, checkSpecFailure) + } + + return output +} + +func markAllValidationsFailed(output checkSpecOutput) { + for k := range output.validation { + output.validation[k] = checkSpecFailure + } +} + +func setValidationStatus(output checkSpecOutput, req checkSpecRequirement, status checkSpecStatus) { + output.validation[req] = status +} + +func validateErrorObject(err *fetcher.Error, output checkSpecOutput) { + if err != nil { + if err.ClientErr != nil && isNegative(int64(err.ClientErr.Code)) { + printError("%v\n", errErrorNegativeCode) + setValidationStatus(output, errorCode, checkSpecFailure) + } + + if err.ClientErr != nil && isEmpty(err.ClientErr.Message) { + printError("%v\n", errErrorEmptyMessage) + setValidationStatus(output, errorMessage, checkSpecFailure) + } + } +} + +func printInfo(format string, a ...interface{}) { + fmt.Printf(format, a...) +} + +func printError(format string, a ...interface{}) { + fmt.Print(color.RedString(format, a...)) +} + +func printSuccess(format string, a ...interface{}) { + fmt.Print(color.GreenString(format, a...)) +} + +func printValidationResult(format string, status checkSpecStatus, a ...interface{}) { + if status == checkSpecFailure { + printError(format, a...) + } else { + printSuccess(format, a...) + } +} + +func printCheckSpecOutputHeader() { + printInfo("%v\n", "+--------------------------+-------------------------------------------------------------------+-----------+") + printInfo("%v\n", "| API | Requirement | Status |") + printInfo("%v\n", "+--------------------------+-------------------------------------------------------------------+-----------+") +} + +func printCheckSpecOutputBody(output checkSpecOutput) { + for k, v := range output.validation { + // print api + printInfo("%v", "| ") + printValidationResult("%v", v, output.api) + for j := 0; j < 24-len(output.api); j++ { + printInfo("%v", " ") + } + + // print requirement description + printInfo("%v", "| ") + printValidationResult("%v", v, k) + for j := 0; j < 65-len(k); j++ { + printInfo(" ") + } + + // print validation status + printInfo("%v", "| ") + printValidationResult("%v", v, v) + for j := 0; j < 9-len(v); j++ { + printInfo("%v", " ") + } + + printInfo("%v\n", "|") + printInfo("%v\n", "+--------------------------+-------------------------------------------------------------------+-----------+") + } } diff --git a/cmd/utils_shared.go b/cmd/utils_shared.go new file mode 100644 index 00000000..97147088 --- /dev/null +++ b/cmd/utils_shared.go @@ -0,0 +1,31 @@ +// Copyright 2022 Coinbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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 cmd + +func isEmpty(s string) bool { + return s == "" +} + +func isNegative(n int64) bool { + return n < 0 +} + +func isEqual(s1 string, s2 string) bool { + return s1 == s2 +} + +func isUTXO() bool { + return Config.CoinSupported +}