From 152148cdef0efae77af996aedd98032c13d857f1 Mon Sep 17 00:00:00 2001 From: NicolaLS Date: Mon, 26 Feb 2024 06:16:47 -0600 Subject: [PATCH 1/2] frontend: implement block explorer selection Add a setting that lets users configure their preferred block explorer. Only whitelisted block explorers are available options. The frontend decides which selections to show (e.g only BTC or BTC and ETH) based on the accounts that are present. --- frontends/web/src/api/backend.ts | 8 + frontends/web/src/locales/en/app.json | 26 ++++ frontends/web/src/routes/account/account.tsx | 16 +- frontends/web/src/routes/router.tsx | 2 + .../src/routes/settings/advanced-settings.tsx | 2 + .../src/routes/settings/block-explorers.tsx | 62 ++++++++ .../select-explorer-setting.tsx | 39 +++++ .../src/routes/settings/select-explorer.tsx | 142 ++++++++++++++++++ 8 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 frontends/web/src/routes/settings/block-explorers.tsx create mode 100644 frontends/web/src/routes/settings/components/advanced-settings/select-explorer-setting.tsx create mode 100644 frontends/web/src/routes/settings/select-explorer.tsx diff --git a/frontends/web/src/api/backend.ts b/frontends/web/src/api/backend.ts index 6ca02e7ff1..7d17586e2b 100644 --- a/frontends/web/src/api/backend.ts +++ b/frontends/web/src/api/backend.ts @@ -19,6 +19,10 @@ import type { FailResponse, SuccessResponse } from './response'; import { apiGet, apiPost } from '@/utils/request'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; + +export type TBlockExplorer = { name: string; url: string }; +export type TAvailableExplorers = Record; + export interface ICoin { coinCode: CoinCode; name: string; @@ -32,6 +36,10 @@ export interface ISuccess { errorCode?: string; } +export const getAvailableExplorers = (): Promise => { + return apiGet('available-explorers'); +}; + export const getSupportedCoins = (): Promise => { return apiGet('supported-coins'); }; diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index e4a52c8aa9..bc1ffe9d11 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -1103,6 +1103,27 @@ "title": "Which servers does this app talk to?" } }, + "settingsBlockExplorer": { + "instructions": { + "link": { + "text": "Guide on how to use a block explorer" + }, + "text": "For a full tutorial, please visit our guide:", + "title": "How do I use a block explorer?" + }, + "options": { + "text": "The BitBoxApp features a protection mechanism designed to prevent the opening of arbitrary links. This serves as a security measure against malicious links.", + "title": "Why can't I enter my own block explorer URL?" + }, + "what": { + "text": "A block explorer lets you dive into the details of the blockchain, helping you understand what's happening. You can choose from various block explorers to find one that suits you.", + "title": "What is this?" + }, + "why": { + "text": "You can use your preferred block explorer to check out your transaction in more detail or to find additional information about the blockchain.", + "title": "Why should I use a block explorer?" + } + }, "title": "Guide", "toggle": { "close": "Close guide", @@ -1638,6 +1659,10 @@ "description": "You can connect to your own Electrum full node.", "title": "Connect your own full node" }, + "explorer": { + "description": "Change to your preferred block explorer.", + "title": "Choose block explorer" + }, "exportLogs": { "description": "Export log file to help with troubleshooting and support.", "title": "Export logs" @@ -1678,6 +1703,7 @@ "title": "Manage notes" }, "restart": "Please re-start the BitBoxApp for the changes to take effect.", + "save": "Save", "services": { "title": "Services" }, diff --git a/frontends/web/src/routes/account/account.tsx b/frontends/web/src/routes/account/account.tsx index cb637d1206..1d687f8ba6 100644 --- a/frontends/web/src/routes/account/account.tsx +++ b/frontends/web/src/routes/account/account.tsx @@ -72,6 +72,7 @@ export const Account = ({ const [uncoveredFunds, setUncoveredFunds] = useState([]); const [stateCode, setStateCode] = useState(); const supportedExchanges = useLoad(getExchangeBuySupported(code), [code]); + const [ blockExplorerTxPrefix, setBlockExplorerTxPrefix ] = useState(); const account = accounts && accounts.find(acct => acct.code === code); @@ -125,8 +126,17 @@ export const Account = ({ useEffect(() => { maybeCheckBitsuranceStatus(); - getConfig().then(({ backend }) => setUsesProxy(backend.proxy.useProxy)); - }, [maybeCheckBitsuranceStatus]); + getConfig().then(({ backend }) => { + setUsesProxy(backend.proxy.useProxy); + if (account) { + if (backend[account.coinCode]) { + setBlockExplorerTxPrefix(backend.blockExplorers[account.coinCode]); + } else { + setBlockExplorerTxPrefix(account.blockExplorerTxPrefix); + } + } + }); + }, [maybeCheckBitsuranceStatus, account]); const hasCard = useSDCard(devices, [code]); @@ -329,7 +339,7 @@ export const Account = ({ {!isAccountEmpty && } diff --git a/frontends/web/src/routes/router.tsx b/frontends/web/src/routes/router.tsx index 1458da3236..e93fe3dd05 100644 --- a/frontends/web/src/routes/router.tsx +++ b/frontends/web/src/routes/router.tsx @@ -45,6 +45,7 @@ import { BitsuranceWidget } from './bitsurance/widget'; import { BitsuranceDashboard } from './bitsurance/dashboard'; import { ConnectScreenWalletConnect } from './account/walletconnect/connect'; import { DashboardWalletConnect } from './account/walletconnect/dashboard'; +import { SelectExplorerSettings } from './settings/select-explorer'; type TAppRouterProps = { devices: TDevices; @@ -249,6 +250,7 @@ export const AppRouter = ({ devices, deviceIDs, devicesKey, accounts, activeAcco } /> + } /> + { hasAccounts ? : null } diff --git a/frontends/web/src/routes/settings/block-explorers.tsx b/frontends/web/src/routes/settings/block-explorers.tsx new file mode 100644 index 0000000000..666bb6d9e9 --- /dev/null +++ b/frontends/web/src/routes/settings/block-explorers.tsx @@ -0,0 +1,62 @@ +/** + * Copyright 2022 Shift Crypto AG + * + * 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. + */ + +import { CoinCode } from '@/api/account'; +import { TBlockExplorer } from '@/api/backend'; +import { SingleDropdown } from './components/dropdowns/singledropdown'; + +type TOption = { + label: string; + value: string; +} + +type TProps = { + coin: CoinCode; + explorerOptions: TBlockExplorer[]; + handleOnChange: (value: string, coin: CoinCode) => void + selectedPrefix: string; +}; + +export const BlockExplorers = ({ coin, explorerOptions, handleOnChange, selectedPrefix }: TProps) => { + const options: TOption[] = explorerOptions.map(explorer => { + return { label: explorer.name, value: explorer.url }; + }); + + const fullCoinName = new Map([ + ['btc', 'Bitcoin'], + ['tbtc', 'Testnet Bitcoin'], + ['ltc', 'Litecoin'], + ['tltc', 'Testnet Litecoin'], + ['eth', 'Ethereum'], + ['goeth', 'Goerli Ethereum'], + ['sepeth', 'Sepolia Ethereum'], + ]); + + // find the index of the currently selected explorer. will be -1 if none is found. + const activeExplorerIndex = explorerOptions.findIndex(explorer => explorer.url === selectedPrefix); + + return ( + options.length > 0 && +
+

{fullCoinName.get(coin)}

+ handleOnChange(value, coin)} + value={options[activeExplorerIndex > 0 ? activeExplorerIndex : 0]} + /> +
+ ); +}; diff --git a/frontends/web/src/routes/settings/components/advanced-settings/select-explorer-setting.tsx b/frontends/web/src/routes/settings/components/advanced-settings/select-explorer-setting.tsx new file mode 100644 index 0000000000..762d79c3a9 --- /dev/null +++ b/frontends/web/src/routes/settings/components/advanced-settings/select-explorer-setting.tsx @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Shift Crypto AG + * + * 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. + */ + +import { useNavigate } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import { ChevronRightDark } from '@/components/icon'; +import { SettingsItem } from '@/routes/settings/components/settingsItem/settingsItem'; + +export const SelectExplorerSetting = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( + navigate('/settings/select-explorer')} + secondaryText={t('settings.expert.explorer.description')} + extraComponent={ + + } + /> + ); +}; diff --git a/frontends/web/src/routes/settings/select-explorer.tsx b/frontends/web/src/routes/settings/select-explorer.tsx new file mode 100644 index 0000000000..b1ddc9444b --- /dev/null +++ b/frontends/web/src/routes/settings/select-explorer.tsx @@ -0,0 +1,142 @@ +/** + * Copyright 2018 Shift Devices AG + * Copyright 2022 Shift Crypto AG + * + * 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. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CoinCode, IAccount } from '@/api/account'; +import * as backendAPI from '@/api/backend'; +import { i18n } from '@/i18n/i18n'; +import { Guide } from '@/components/guide/guide'; +import { Entry } from '@/components/guide/entry'; +import { Button, ButtonLink } from '@/components/forms'; +import { Header } from '@/components/layout'; +import { getConfig, setConfig } from '@/utils/config'; +import { MobileHeader } from './components/mobile-header'; +import { BlockExplorers } from './block-explorers'; + +type TSelectExplorerSettingsProps = { + accounts: IAccount[]; +} + +export const SelectExplorerSettings = ({ accounts }: TSelectExplorerSettingsProps) => { + const { t } = useTranslation(); + + const initialConfig = useRef(); + const [config, setConfigState] = useState(); + + const availableCoins = new Set(accounts.map(account => account.coinCode)); + const [allSelections, setAllSelections] = useState(); + + const [saveDisabled, setSaveDisabled] = useState(true); + + const loadConfig = () => { + getConfig().then(setConfigState); + }; + + + const updateConfigState = useCallback((newConfig: any) => { + if (JSON.stringify(initialConfig.current) !== JSON.stringify(newConfig)) { + setConfigState(newConfig); + setSaveDisabled(false); + } else { + setSaveDisabled(true); + } + }, []); + + const handleChange = useCallback((selectedTxPrefix: string, coin: CoinCode) => { + if (config.backend.blockExplorers[coin] && config.backend.blockExplorers[coin] !== selectedTxPrefix) { + config.backend.blockExplorers[coin] = selectedTxPrefix; + updateConfigState(config); + } + }, [config, updateConfigState]); + + const save = async () => { + setSaveDisabled(true); + await setConfig(config); + initialConfig.current = await getConfig(); + }; + + useEffect(() => { + const fetchData = async () => { + const allExplorerSelection = await backendAPI.getAvailableExplorers(); + + // if set alongside config it will 'update' with it, but we want it to stay the same after initialization. + initialConfig.current = await getConfig(); + + setAllSelections(allExplorerSelection); + }; + + loadConfig(); + fetchData().catch(console.error); + }, []); + + if (config === undefined) { + return null; + } + + return ( +
+
+
+
+

{t('settings.expert.explorer.title')}

+ + + }/> +
+ { Array.from(availableCoins).map(coin => { + return ; + }) } +
+
+ + {t('button.back')} + + +
+
+
+ + + + + + +
+ ); +}; From bedadc8a14d8ac733a905a3e86435f1ebb3bbb80 Mon Sep 17 00:00:00 2001 From: NicolaLS Date: Mon, 26 Feb 2024 06:30:12 -0600 Subject: [PATCH 2/2] 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 for which a handler was implemented in this commit. --- backend/backend.go | 50 ++++++++++++------ backend/config/blockexplorer.go | 91 +++++++++++++++++++++++++++++++++ backend/config/config.go | 21 ++++++++ backend/handlers/handlers.go | 8 +++ 4 files changed, 154 insertions(+), 16 deletions(-) create mode 100644 backend/config/blockexplorer.go diff --git a/backend/backend.go b/backend/backend.go index 36fc3ad82a..2f7775bdd2 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -22,6 +22,7 @@ import ( "net/url" "os" "path/filepath" + "reflect" "strings" "time" @@ -71,14 +72,7 @@ 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/", - "https://sepolia.etherscan.io/tx/", + // Moonpay onramp "https://www.moonpay.com/", "https://support.moonpay.com/", @@ -490,43 +484,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, ) @@ -827,6 +829,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) } @@ -997,4 +1009,10 @@ func (backend *Backend) ExportLogs() error { return err } return nil + +} + +// 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..0da302c305 --- /dev/null +++ b/backend/config/blockexplorer.go @@ -0,0 +1,91 @@ +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 holds all available block explorers for each coin. +// It is returned from the available-explorers endpoint. +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 8ea963402e..ead276e27e 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -25,6 +25,16 @@ import ( "github.com/BitBoxSwiss/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"` @@ -83,6 +93,8 @@ type Backend struct { TLTC btcCoinConfig `json:"tltc"` ETH ethCoinConfig `json:"eth"` + BlockExplorers blockExplorers `json:"blockExplorers"` + // Removed in v4.35 - don't reuse these two keys. TETH struct{} `json:"teth"` RETH struct{} `json:"reth"` @@ -228,6 +240,15 @@ func NewDefaultAppConfig() AppConfig { ETH: 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 318a910c31..55d51ba4ec 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -107,6 +107,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) @@ -257,6 +258,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/accounts/eth-account-code", handlers.lookupEthAccountCode).Methods("POST") getAPIRouterNoError(apiRouter)("/notes/export", handlers.postExportNotes).Methods("POST") getAPIRouterNoError(apiRouter)("/notes/import", handlers.postImportNotes).Methods("POST") + getAPIRouterNoError(apiRouter)("/available-explorers", handlers.getAvailableExplorers).Methods("GET") devicesRouter := getAPIRouterNoError(apiRouter.PathPrefix("/devices").Subrouter()) devicesRouter("/registered", handlers.getDevicesRegistered).Methods("GET") @@ -1524,3 +1526,9 @@ func (handlers *Handlers) postImportNotes(r *http.Request) interface{} { } return result{Success: true, Data: data} } + +// getAvailableExplorers returns a struct containing arrays with block explorers for each +// individual coin code. +func (handlers *Handlers) getAvailableExplorers(*http.Request) interface{} { + return config.AvailableExplorers +}