Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block explorer selection #2579

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"time"

Expand Down Expand Up @@ -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/",
Expand Down Expand Up @@ -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, &ltc.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, &ltc.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,
)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
91 changes: 91 additions & 0 deletions backend/config/blockexplorer.go
Original file line number Diff line number Diff line change
@@ -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/",
},
},
}
21 changes: 21 additions & 0 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions backend/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
8 changes: 8 additions & 0 deletions frontends/web/src/api/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoinCode, TBlockExplorer[]>;

export interface ICoin {
coinCode: CoinCode;
name: string;
Expand All @@ -32,6 +36,10 @@ export interface ISuccess {
errorCode?: string;
}

export const getAvailableExplorers = (): Promise<TAvailableExplorers> => {
return apiGet('available-explorers');
};

export const getSupportedCoins = (): Promise<ICoin[]> => {
return apiGet('supported-coins');
};
Expand Down
26 changes: 26 additions & 0 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -1678,6 +1703,7 @@
"title": "Manage notes"
},
"restart": "Please re-start the BitBoxApp for the changes to take effect.",
"save": "Save",
"services": {
"title": "Services"
},
Expand Down
16 changes: 13 additions & 3 deletions frontends/web/src/routes/account/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const Account = ({
const [uncoveredFunds, setUncoveredFunds] = useState<string[]>([]);
const [stateCode, setStateCode] = useState<string>();
const supportedExchanges = useLoad<SupportedExchanges>(getExchangeBuySupported(code), [code]);
const [ blockExplorerTxPrefix, setBlockExplorerTxPrefix ] = useState<string>();

const account = accounts && accounts.find(acct => acct.code === code);

Expand Down Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is redundant as we check blockExplorerTxPrefix undefined on line 337. Should I remove the else branch ?

setBlockExplorerTxPrefix(account.blockExplorerTxPrefix);
}
}
});
}, [maybeCheckBitsuranceStatus, account]);

const hasCard = useSDCard(devices, [code]);

Expand Down Expand Up @@ -329,7 +339,7 @@ export const Account = ({
{!isAccountEmpty && <Transactions
accountCode={code}
handleExport={exportAccount}
explorerURL={account.blockExplorerTxPrefix}
explorerURL={blockExplorerTxPrefix ? blockExplorerTxPrefix : account.blockExplorerTxPrefix }
transactions={transactions}
/>}
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontends/web/src/routes/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -249,6 +250,7 @@ export const AppRouter = ({ devices, deviceIDs, devicesKey, accounts, activeAcco
<Route path="device-settings/bip85/:deviceID" element={Bip85El} />
<Route path="advanced-settings" element={AdvancedSettingsEl} />
<Route path="electrum" element={<ElectrumSettings />} />
<Route path="select-explorer" element={<SelectExplorerSettings accounts={accounts}/>} />
<Route path="manage-accounts" element={
<ManageAccounts
accounts={accounts}
Expand Down
Loading
Loading