Skip to content

Commit 239b7fa

Browse files
committed
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.
1 parent d8faff0 commit 239b7fa

File tree

4 files changed

+154
-16
lines changed

4 files changed

+154
-16
lines changed

backend/backend.go

+34-16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net/url"
2323
"os"
2424
"path/filepath"
25+
"reflect"
2526
"strings"
2627
"time"
2728

@@ -71,14 +72,7 @@ var fixedURLWhitelist = []string{
7172
"https://shiftcrypto.support/",
7273
// Exchange rates.
7374
"https://www.coingecko.com/",
74-
// Block explorers.
75-
"https://blockstream.info/tx/",
76-
"https://blockstream.info/testnet/tx/",
77-
"https://sochain.com/tx/LTCTEST/",
78-
"https://blockchair.com/litecoin/transaction/",
79-
"https://etherscan.io/tx/",
80-
"https://goerli.etherscan.io/tx/",
81-
"https://sepolia.etherscan.io/tx/",
75+
8276
// Moonpay onramp
8377
"https://www.moonpay.com/",
8478
"https://support.moonpay.com/",
@@ -490,43 +484,51 @@ func (backend *Backend) Coin(code coinpkg.Code) (coinpkg.Coin, error) {
490484
servers := backend.defaultElectrumXServers(code)
491485
coin = btc.NewCoin(coinpkg.CodeRBTC, "Bitcoin Regtest", "RBTC", coinpkg.BtcUnitDefault, &chaincfg.RegressionNetParams, dbFolder, servers, "", backend.socksProxy)
492486
case code == coinpkg.CodeTBTC:
487+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.TBTC
493488
servers := backend.defaultElectrumXServers(code)
494489
coin = btc.NewCoin(coinpkg.CodeTBTC, "Bitcoin Testnet", "TBTC", btcFormatUnit, &chaincfg.TestNet3Params, dbFolder, servers,
495-
"https://blockstream.info/testnet/tx/", backend.socksProxy)
490+
blockExplorerPrefix, backend.socksProxy)
496491
case code == coinpkg.CodeBTC:
492+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.BTC
497493
servers := backend.defaultElectrumXServers(code)
498494
coin = btc.NewCoin(coinpkg.CodeBTC, "Bitcoin", "BTC", btcFormatUnit, &chaincfg.MainNetParams, dbFolder, servers,
499-
"https://blockstream.info/tx/", backend.socksProxy)
495+
blockExplorerPrefix, backend.socksProxy)
500496
case code == coinpkg.CodeTLTC:
497+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.TLTC
501498
servers := backend.defaultElectrumXServers(code)
502499
coin = btc.NewCoin(coinpkg.CodeTLTC, "Litecoin Testnet", "TLTC", coinpkg.BtcUnitDefault, &ltc.TestNet4Params, dbFolder, servers,
503-
"https://sochain.com/tx/LTCTEST/", backend.socksProxy)
500+
blockExplorerPrefix, backend.socksProxy)
504501
case code == coinpkg.CodeLTC:
502+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.LTC
505503
servers := backend.defaultElectrumXServers(code)
506504
coin = btc.NewCoin(coinpkg.CodeLTC, "Litecoin", "LTC", coinpkg.BtcUnitDefault, &ltc.MainNetParams, dbFolder, servers,
507-
"https://blockchair.com/litecoin/transaction/", backend.socksProxy)
505+
blockExplorerPrefix, backend.socksProxy)
508506
case code == coinpkg.CodeETH:
507+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.ETH
509508
etherScan := etherscan.NewEtherScan("https://api.etherscan.io/api", backend.etherScanHTTPClient)
510509
coin = eth.NewCoin(etherScan, code, "Ethereum", "ETH", "ETH", params.MainnetChainConfig,
511-
"https://etherscan.io/tx/",
510+
blockExplorerPrefix,
512511
etherScan,
513512
nil)
514513
case code == coinpkg.CodeGOETH:
514+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.GOETH
515515
etherScan := etherscan.NewEtherScan("https://api-goerli.etherscan.io/api", backend.etherScanHTTPClient)
516516
coin = eth.NewCoin(etherScan, code, "Ethereum Goerli", "GOETH", "GOETH", params.GoerliChainConfig,
517-
"https://goerli.etherscan.io/tx/",
517+
blockExplorerPrefix,
518518
etherScan,
519519
nil)
520520
case code == coinpkg.CodeSEPETH:
521+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.SEPETH
521522
etherScan := etherscan.NewEtherScan("https://api-sepolia.etherscan.io/api", backend.etherScanHTTPClient)
522523
coin = eth.NewCoin(etherScan, code, "Ethereum Sepolia", "SEPETH", "SEPETH", params.SepoliaChainConfig,
523-
"https://sepolia.etherscan.io/tx/",
524+
blockExplorerPrefix,
524525
etherScan,
525526
nil)
526527
case erc20Token != nil:
528+
blockExplorerPrefix := backend.config.AppConfig().Backend.BlockExplorers.ETH
527529
etherScan := etherscan.NewEtherScan("https://api.etherscan.io/api", backend.etherScanHTTPClient)
528530
coin = eth.NewCoin(etherScan, erc20Token.code, erc20Token.name, erc20Token.unit, "ETH", params.MainnetChainConfig,
529-
"https://etherscan.io/tx/",
531+
blockExplorerPrefix,
530532
etherScan,
531533
erc20Token.token,
532534
)
@@ -827,6 +829,16 @@ func (backend *Backend) SystemOpen(url string) error {
827829
}
828830
}
829831

832+
// Block explorers are not defined in the fixedURLWhiteList but in AvailableBlockexplorers.
833+
var allAvailableExplorers = reflect.ValueOf(config.AvailableExplorers)
834+
for i := 0; i < allAvailableExplorers.NumField(); i++ {
835+
coinAvailableExplorers := allAvailableExplorers.Field(i).Interface().([]config.BlockExplorer)
836+
for _, explorer := range coinAvailableExplorers {
837+
if strings.HasPrefix(url, explorer.Url) {
838+
return backend.environment.SystemOpen(url)
839+
}
840+
}
841+
}
830842
return errp.Newf("Blocked /open with url: %s", url)
831843
}
832844

@@ -997,4 +1009,10 @@ func (backend *Backend) ExportLogs() error {
9971009
return err
9981010
}
9991011
return nil
1012+
1013+
}
1014+
1015+
// AvailableExplorers returns a struct containing all available block explorers for each coin.
1016+
func (backend *Backend) AvailableExplorers() config.AvailableBlockExplorers {
1017+
return config.AvailableExplorers
10001018
}

backend/config/blockexplorer.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package config
2+
3+
// BlockExplorer defines a selectable block explorer.
4+
type BlockExplorer struct {
5+
// Name of the block explorer used for UI.
6+
Name string `json:"name"`
7+
// Url of the block explorer that the txid is appended.
8+
Url string `json:"url"`
9+
}
10+
11+
// AvailableBlockExplorers defines all available block explorers for each coin.
12+
type AvailableBlockExplorers struct {
13+
Btc []BlockExplorer `json:"btc"`
14+
Tbtc []BlockExplorer `json:"tbtc"`
15+
Ltc []BlockExplorer `json:"ltc"`
16+
Tltc []BlockExplorer `json:"tltc"`
17+
Eth []BlockExplorer `json:"eth"`
18+
GoEth []BlockExplorer `json:"goeth"`
19+
SepEth []BlockExplorer `json:"sepeth"`
20+
}
21+
22+
// AvailableExplorers holds all available block explorers for each coin.
23+
// It is returned from the available-explorers endpoint.
24+
var AvailableExplorers = AvailableBlockExplorers{
25+
Btc: []BlockExplorer{
26+
{
27+
Name: "blockstream.info",
28+
Url: "https://blockstream.info/tx/",
29+
},
30+
{
31+
Name: "mempool.space",
32+
Url: "https://mempool.space/tx",
33+
},
34+
},
35+
Tbtc: []BlockExplorer{
36+
{
37+
Name: "mempool.space",
38+
Url: "https://mempool.space/testnet/tx/",
39+
},
40+
{
41+
Name: "blockstream.info",
42+
Url: "https://blockstream.info/testnet/tx/",
43+
},
44+
},
45+
Ltc: []BlockExplorer{
46+
{
47+
Name: "sochain.com",
48+
Url: "https://sochain.com/tx/",
49+
},
50+
{
51+
Name: "blockchair.com",
52+
Url: "https://blockchair.com/litecoin/transaction",
53+
},
54+
},
55+
Tltc: []BlockExplorer{
56+
{
57+
Name: "sochain.com",
58+
Url: "https://sochain.com/tx/LTCTEST/",
59+
},
60+
},
61+
Eth: []BlockExplorer{
62+
{
63+
Name: "etherscan.io",
64+
Url: "https://etherscan.io/tx/",
65+
},
66+
{
67+
Name: "ethplorer.io",
68+
Url: "https://ethplorer.io/tx/",
69+
},
70+
},
71+
GoEth: []BlockExplorer{
72+
{
73+
Name: "etherscan.io",
74+
Url: "https://goerli.etherscan.io/tx/",
75+
},
76+
{
77+
Name: "ethplorer.io",
78+
Url: "https://goerli.ethplorer.io/tx/",
79+
},
80+
},
81+
SepEth: []BlockExplorer{
82+
{
83+
Name: "etherscan.io",
84+
Url: "https://sepolia.etherscan.io/tx/",
85+
},
86+
{
87+
Name: "ethplorer.io",
88+
Url: "https://sepolia.ethplorer.io/tx/",
89+
},
90+
},
91+
}

backend/config/config.go

+21
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ import (
2525
"github.com/BitBoxSwiss/bitbox-wallet-app/util/locker"
2626
)
2727

28+
type blockExplorers struct {
29+
BTC string `json:"btc"`
30+
TBTC string `json:"tbtc"`
31+
LTC string `json:"ltc"`
32+
TLTC string `json:"tltc"`
33+
ETH string `json:"eth"`
34+
GOETH string `json:"goeth"`
35+
SEPETH string `json:"sepeth"`
36+
}
37+
2838
// ServerInfo holds information about the backend server(s).
2939
type ServerInfo struct {
3040
Server string `json:"server"`
@@ -83,6 +93,8 @@ type Backend struct {
8393
TLTC btcCoinConfig `json:"tltc"`
8494
ETH ethCoinConfig `json:"eth"`
8595

96+
BlockExplorers blockExplorers `json:"blockExplorers"`
97+
8698
// Removed in v4.35 - don't reuse these two keys.
8799
TETH struct{} `json:"teth"`
88100
RETH struct{} `json:"reth"`
@@ -228,6 +240,15 @@ func NewDefaultAppConfig() AppConfig {
228240
ETH: ethCoinConfig{
229241
DeprecatedActiveERC20Tokens: []string{},
230242
},
243+
BlockExplorers: blockExplorers{
244+
BTC: AvailableExplorers.Btc[0].Url,
245+
TBTC: AvailableExplorers.Tbtc[0].Url,
246+
LTC: AvailableExplorers.Ltc[0].Url,
247+
TLTC: AvailableExplorers.Tltc[0].Url,
248+
ETH: AvailableExplorers.Eth[0].Url,
249+
GOETH: AvailableExplorers.GoEth[0].Url,
250+
SEPETH: AvailableExplorers.SepEth[0].Url,
251+
},
231252
// Copied from frontend/web/src/components/rates/rates.tsx.
232253
FiatList: []string{rates.USD.String(), rates.EUR.String(), rates.CHF.String()},
233254
MainFiat: rates.USD.String(),

backend/handlers/handlers.go

+8
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type Backend interface {
107107
AOPPCancel()
108108
AOPPApprove()
109109
AOPPChooseAccount(code accountsTypes.Code)
110+
AvailableExplorers() config.AvailableBlockExplorers
110111
GetAccountFromCode(code accountsTypes.Code) (accounts.Interface, error)
111112
HTTPClient() *http.Client
112113
LookupInsuredAccounts(accountCode accountsTypes.Code) ([]bitsurance.AccountDetails, error)
@@ -257,6 +258,7 @@ func NewHandlers(
257258
getAPIRouterNoError(apiRouter)("/accounts/eth-account-code", handlers.lookupEthAccountCode).Methods("POST")
258259
getAPIRouterNoError(apiRouter)("/notes/export", handlers.postExportNotes).Methods("POST")
259260
getAPIRouterNoError(apiRouter)("/notes/import", handlers.postImportNotes).Methods("POST")
261+
getAPIRouterNoError(apiRouter)("/available-explorers", handlers.getAvailableExplorers).Methods("GET")
260262

261263
devicesRouter := getAPIRouterNoError(apiRouter.PathPrefix("/devices").Subrouter())
262264
devicesRouter("/registered", handlers.getDevicesRegistered).Methods("GET")
@@ -1513,3 +1515,9 @@ func (handlers *Handlers) postImportNotes(r *http.Request) interface{} {
15131515
}
15141516
return result{Success: true, Data: data}
15151517
}
1518+
1519+
// getAvailableExplorers returns a struct containing arrays with block explorers for each
1520+
// individual coin code.
1521+
func (handlers *Handlers) getAvailableExplorers(*http.Request) interface{} {
1522+
return config.AvailableExplorers
1523+
}

0 commit comments

Comments
 (0)