From f45d117286df16b3fb1f2e6791875004b36f9ade Mon Sep 17 00:00:00 2001 From: NicolaLS Date: Mon, 26 Feb 2024 06:30:12 -0600 Subject: [PATCH] backend: implement block explorer selection Add the block explorer prefix url to the configuration that will be used to open the transaction. The available options to select are statically defined and the frontend can learn them by calling the available-explorers endpoint. --- backend/backend.go | 47 +++++++++++------ backend/config/blockexplorer.go | 90 +++++++++++++++++++++++++++++++++ backend/config/config.go | 41 ++++++++++++--- backend/handlers/handlers.go | 9 ++++ 4 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 backend/config/blockexplorer.go diff --git a/backend/backend.go b/backend/backend.go index afb4748688..54511830e8 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -23,6 +23,7 @@ import ( "net/url" "os" "path/filepath" + "reflect" "strings" "time" @@ -73,13 +74,6 @@ var fixedURLWhitelist = []string{ "https://shiftcrypto.support/", // Exchange rates. "https://www.coingecko.com/", - // Block explorers. - "https://blockstream.info/tx/", - "https://blockstream.info/testnet/tx/", - "https://sochain.com/tx/LTCTEST/", - "https://blockchair.com/litecoin/transaction/", - "https://etherscan.io/tx/", - "https://goerli.etherscan.io/tx/", // Moonpay onramp "https://www.moonpay.com/", "https://support.moonpay.com/", @@ -485,43 +479,51 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) { servers := backend.defaultElectrumXServers(code) coin = btc.NewCoin(coinpkg.CodeRBTC, "Bitcoin Regtest", "RBTC", coinpkg.BtcUnitDefault, &chaincfg.RegressionNetParams, dbFolder, servers, "", backend.socksProxy) case code == coinpkg.CodeTBTC: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.TBTC servers := backend.defaultElectrumXServers(code) coin = btc.NewCoin(coinpkg.CodeTBTC, "Bitcoin Testnet", "TBTC", btcFormatUnit, &chaincfg.TestNet3Params, dbFolder, servers, - "https://blockstream.info/testnet/tx/", backend.socksProxy) + blockExplorerPrefix, backend.socksProxy) case code == coinpkg.CodeBTC: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.BTC servers := backend.defaultElectrumXServers(code) coin = btc.NewCoin(coinpkg.CodeBTC, "Bitcoin", "BTC", btcFormatUnit, &chaincfg.MainNetParams, dbFolder, servers, - "https://blockstream.info/tx/", backend.socksProxy) + blockExplorerPrefix, backend.socksProxy) case code == coinpkg.CodeTLTC: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.TLTC servers := backend.defaultElectrumXServers(code) coin = btc.NewCoin(coinpkg.CodeTLTC, "Litecoin Testnet", "TLTC", coinpkg.BtcUnitDefault, <c.TestNet4Params, dbFolder, servers, - "https://sochain.com/tx/LTCTEST/", backend.socksProxy) + blockExplorerPrefix, backend.socksProxy) case code == coinpkg.CodeLTC: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.LTC servers := backend.defaultElectrumXServers(code) coin = btc.NewCoin(coinpkg.CodeLTC, "Litecoin", "LTC", coinpkg.BtcUnitDefault, <c.MainNetParams, dbFolder, servers, - "https://blockchair.com/litecoin/transaction/", backend.socksProxy) + blockExplorerPrefix, backend.socksProxy) case code == coinpkg.CodeETH: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.ETH etherScan := etherscan.NewEtherScan("https://api.etherscan.io/api", backend.etherScanHTTPClient) coin = eth.NewCoin(etherScan, code, "Ethereum", "ETH", "ETH", params.MainnetChainConfig, - "https://etherscan.io/tx/", + blockExplorerPrefix, etherScan, nil) case code == coinpkg.CodeGOETH: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.GOETH etherScan := etherscan.NewEtherScan("https://api-goerli.etherscan.io/api", backend.etherScanHTTPClient) coin = eth.NewCoin(etherScan, code, "Ethereum Goerli", "GOETH", "GOETH", params.GoerliChainConfig, - "https://goerli.etherscan.io/tx/", + blockExplorerPrefix, etherScan, nil) case code == coinpkg.CodeSEPETH: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.SEPETH etherScan := etherscan.NewEtherScan("https://api-sepolia.etherscan.io/api", backend.etherScanHTTPClient) coin = eth.NewCoin(etherScan, code, "Ethereum Sepolia", "SEPETH", "SEPETH", params.SepoliaChainConfig, - "https://sepolia.etherscan.io/tx/", + blockExplorerPrefix, etherScan, nil) case erc20Token != nil: + blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.ETH etherScan := etherscan.NewEtherScan("https://api.etherscan.io/api", backend.etherScanHTTPClient) coin = eth.NewCoin(etherScan, erc20Token.code, erc20Token.name, erc20Token.unit, "ETH", params.MainnetChainConfig, - "https://etherscan.io/tx/", + blockExplorerPrefix, etherScan, erc20Token.token, ) @@ -905,6 +907,16 @@ func (backend *Backend) SystemOpen(url string) error { } } + // Block explorers are not defined in the fixedURLWhiteList but in AvailableBlockexplorers. + var allAvailableExplorers = reflect.ValueOf(config.AvailableExplorers) + for i := 0; i < allAvailableExplorers.NumField(); i++ { + coinAvailableExplorers := allAvailableExplorers.Field(i).Interface().([]config.BlockExplorer) + for _, explorer := range coinAvailableExplorers { + if strings.HasPrefix(url, explorer.Url) { + return backend.environment.SystemOpen(url) + } + } + } return errp.Newf("Blocked /open with url: %s", url) } @@ -1028,3 +1040,8 @@ func (backend *Backend) SetWatchonly(rootFingerprint []byte, watchonly bool) err &t, ) } + +// AvailableExplorers returns a struct containing all available block explorers for each coin. +func (backend *Backend) AvailableExplorers() config.AvailableBlockExplorers { + return config.AvailableExplorers +} diff --git a/backend/config/blockexplorer.go b/backend/config/blockexplorer.go new file mode 100644 index 0000000000..a54a9bc082 --- /dev/null +++ b/backend/config/blockexplorer.go @@ -0,0 +1,90 @@ +package config + +// BlockExplorer defines a selectable block explorer. +type BlockExplorer struct { + // Name of the block explorer used for UI. + Name string `json:"name"` + // Url of the block explorer that the txid is appended. + Url string `json:"url"` +} + +// AvailableBlockExplorers defines all available block explorers for each coin. +type AvailableBlockExplorers struct { + Btc []BlockExplorer `json:"btc"` + Tbtc []BlockExplorer `json:"tbtc"` + Ltc []BlockExplorer `json:"ltc"` + Tltc []BlockExplorer `json:"tltc"` + Eth []BlockExplorer `json:"eth"` + GoEth []BlockExplorer `json:"goeth"` + SepEth []BlockExplorer `json:"sepeth"` +} + +// AvailableExplorers FIXME: Localize AvailableExplorers. +var AvailableExplorers = AvailableBlockExplorers{ + Btc: []BlockExplorer{ + { + Name: "blockstream.info", + Url: "https://blockstream.info/tx/", + }, + { + Name: "mempool.space", + Url: "https://mempool.space/tx", + }, + }, + Tbtc: []BlockExplorer{ + { + Name: "mempool.space", + Url: "https://mempool.space/testnet/tx/", + }, + { + Name: "blockstream.info", + Url: "https://blockstream.info/testnet/tx/", + }, + }, + Ltc: []BlockExplorer{ + { + Name: "sochain.com", + Url: "https://sochain.com/tx/", + }, + { + Name: "blockchair.com", + Url: "https://blockchair.com/litecoin/transaction", + }, + }, + Tltc: []BlockExplorer{ + { + Name: "sochain.com", + Url: "https://sochain.com/tx/LTCTEST/", + }, + }, + Eth: []BlockExplorer{ + { + Name: "etherscan.io", + Url: "https://etherscan.io/tx/", + }, + { + Name: "ethplorer.io", + Url: "https://ethplorer.io/tx/", + }, + }, + GoEth: []BlockExplorer{ + { + Name: "etherscan.io", + Url: "https://goerli.etherscan.io/tx/", + }, + { + Name: "ethplorer.io", + Url: "https://goerli.ethplorer.io/tx/", + }, + }, + SepEth: []BlockExplorer{ + { + Name: "etherscan.io", + Url: "https://sepolia.etherscan.io/tx/", + }, + { + Name: "ethplorer.io", + Url: "https://sepolia.ethplorer.io/tx/", + }, + }, +} diff --git a/backend/config/config.go b/backend/config/config.go index c051cc48ce..f3e274cf65 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -25,6 +25,16 @@ import ( "github.com/digitalbitbox/bitbox-wallet-app/util/locker" ) +type blockExplorers struct { + BTC string `json:"btc"` + TBTC string `json:"tbtc"` + LTC string `json:"ltc"` + TLTC string `json:"tltc"` + ETH string `json:"eth"` + GOETH string `json:"goeth"` + SEPETH string `json:"sepeth"` +} + // ServerInfo holds information about the backend server(s). type ServerInfo struct { Server string `json:"server"` @@ -76,12 +86,16 @@ type Backend struct { Authentication bool `json:"authentication"` - BTC btcCoinConfig `json:"btc"` - TBTC btcCoinConfig `json:"tbtc"` - RBTC btcCoinConfig `json:"rbtc"` - LTC btcCoinConfig `json:"ltc"` - TLTC btcCoinConfig `json:"tltc"` - ETH ethCoinConfig `json:"eth"` + BTC btcCoinConfig `json:"btc"` + TBTC btcCoinConfig `json:"tbtc"` + RBTC btcCoinConfig `json:"rbtc"` + LTC btcCoinConfig `json:"ltc"` + TLTC btcCoinConfig `json:"tltc"` + ETH ethCoinConfig `json:"eth"` + GOETH ethCoinConfig `json:"goeth"` + SEPETH ethCoinConfig `json:"sepeth"` + + BlockExplorers blockExplorers `json:"blockExplorers"` // Removed in v4.35 - don't reuse these two keys. TETH struct{} `json:"teth"` @@ -228,6 +242,21 @@ func NewDefaultAppConfig() AppConfig { ETH: ethCoinConfig{ DeprecatedActiveERC20Tokens: []string{}, }, + GOETH: ethCoinConfig{ + DeprecatedActiveERC20Tokens: []string{}, + }, + SEPETH: ethCoinConfig{ + DeprecatedActiveERC20Tokens: []string{}, + }, + BlockExplorers: blockExplorers{ + BTC: AvailableExplorers.Btc[0].Url, + TBTC: AvailableExplorers.Tbtc[0].Url, + LTC: AvailableExplorers.Ltc[0].Url, + TLTC: AvailableExplorers.Tltc[0].Url, + ETH: AvailableExplorers.Eth[0].Url, + GOETH: AvailableExplorers.GoEth[0].Url, + SEPETH: AvailableExplorers.SepEth[0].Url, + }, // Copied from frontend/web/src/components/rates/rates.tsx. FiatList: []string{rates.USD.String(), rates.EUR.String(), rates.CHF.String()}, MainFiat: rates.USD.String(), diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 8876284b5a..f7778fe32f 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -104,6 +104,7 @@ type Backend interface { AOPPCancel() AOPPApprove() AOPPChooseAccount(code accountsTypes.Code) + AvailableExplorers() config.AvailableBlockExplorers GetAccountFromCode(code accountsTypes.Code) (accounts.Interface, error) HTTPClient() *http.Client LookupInsuredAccounts(accountCode accountsTypes.Code) ([]bitsurance.AccountDetails, error) @@ -250,6 +251,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/set-watchonly", handlers.postSetWatchonly).Methods("POST") getAPIRouterNoError(apiRouter)("/on-auth-setting-changed", handlers.postOnAuthSettingChanged).Methods("POST") getAPIRouterNoError(apiRouter)("/accounts/eth-account-code", handlers.lookupEthAccountCode).Methods("POST") + getAPIRouterNoError(apiRouter)("/available-explorers", handlers.getAvailableExplorers).Methods("GET") devicesRouter := getAPIRouterNoError(apiRouter.PathPrefix("/devices").Subrouter()) devicesRouter("/registered", handlers.getDevicesRegistered).Methods("GET") @@ -1400,3 +1402,10 @@ func (handlers *Handlers) postOnAuthSettingChanged(r *http.Request) interface{} handlers.backend.Config().AppConfig().Backend.Authentication) return nil } + +// getAvailableExplorers returns a struct containing arrays with block explorers for each +// individual coin code. +func (handlers *Handlers) getAvailableExplorers(*http.Request) interface{} { + // TODO: maybe filter out testing coins if not testing and real if testing + return config.AvailableExplorers +}