Remove global cache variable. (#341)

* Remove global cache variable.

Rather than maintaining cached data in a global variable, instantiate a cache struct and keep it in the `Server` struct.

* Store net params in RPC clients.

This means net params only need to be supplied once at startup, and also removes a global instance of net params in `background.go`.
This commit is contained in:
Jamie Holdstock 2022-03-30 17:00:42 +01:00 committed by GitHub
parent 78abc59e97
commit 78bb28056c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 114 deletions

View File

@ -12,7 +12,6 @@ import (
"sync"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
"github.com/jrick/wsrpc/v2"
@ -22,7 +21,6 @@ var (
db *database.VspDatabase
dcrdRPC rpc.DcrdConnect
walletRPC rpc.WalletConnect
netParams *chaincfg.Params
notifierClosed chan struct{}
)
@ -76,7 +74,7 @@ func blockConnected() {
ctx := context.Background()
dcrdClient, _, err := dcrdRPC.Client(ctx, netParams)
dcrdClient, _, err := dcrdRPC.Client(ctx)
if err != nil {
log.Errorf("%s: %v", funcName, err)
return
@ -170,7 +168,7 @@ func blockConnected() {
log.Errorf("%s: db.GetUnconfirmedFees error: %v", funcName, err)
}
walletClients, failedConnections := walletRPC.Clients(ctx, netParams)
walletClients, failedConnections := walletRPC.Clients(ctx)
if len(walletClients) == 0 {
log.Errorf("%s: Could not connect to any wallets", funcName)
return
@ -333,7 +331,7 @@ func blockConnected() {
func connectNotifier(shutdownCtx context.Context, dcrdWithNotifs rpc.DcrdConnect) error {
notifierClosed = make(chan struct{})
dcrdClient, _, err := dcrdWithNotifs.Client(shutdownCtx, netParams)
dcrdClient, _, err := dcrdWithNotifs.Client(shutdownCtx)
if err != nil {
return err
}
@ -361,12 +359,11 @@ func connectNotifier(shutdownCtx context.Context, dcrdWithNotifs rpc.DcrdConnect
}
func Start(shutdownCtx context.Context, wg *sync.WaitGroup, vdb *database.VspDatabase, drpc rpc.DcrdConnect,
dcrdWithNotif rpc.DcrdConnect, wrpc rpc.WalletConnect, p *chaincfg.Params) {
dcrdWithNotif rpc.DcrdConnect, wrpc rpc.WalletConnect) {
db = vdb
dcrdRPC = drpc
walletRPC = wrpc
netParams = p
// Run the block connected handler now to catch up with any blocks mined
// while vspd was shut down.
@ -428,13 +425,13 @@ func checkWalletConsistency() {
ctx := context.Background()
dcrdClient, _, err := dcrdRPC.Client(ctx, netParams)
dcrdClient, _, err := dcrdRPC.Client(ctx)
if err != nil {
log.Errorf("%s: %v", funcName, err)
return
}
walletClients, failedConnections := walletRPC.Clients(ctx, netParams)
walletClients, failedConnections := walletRPC.Clients(ctx)
if len(walletClients) == 0 {
log.Errorf("%s: Could not connect to any wallets", funcName)
return

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -16,7 +16,6 @@ import (
"sync"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/vspd/rpc"
bolt "go.etcd.io/bbolt"
)
@ -386,7 +385,7 @@ func (vdb *VspDatabase) BackupDB(w http.ResponseWriter) error {
// CheckIntegrity will ensure that all data in the database is present and up to
// date.
func (vdb *VspDatabase) CheckIntegrity(ctx context.Context, params *chaincfg.Params, dcrd rpc.DcrdConnect) error {
func (vdb *VspDatabase) CheckIntegrity(ctx context.Context, dcrd rpc.DcrdConnect) error {
// Ensure all confirmed tickets have a purchase height.
// This is necessary because of an old bug which, in some circumstances,
@ -401,7 +400,7 @@ func (vdb *VspDatabase) CheckIntegrity(ctx context.Context, params *chaincfg.Par
return nil
}
dcrdClient, _, err := dcrd.Client(ctx, params)
dcrdClient, _, err := dcrd.Client(ctx)
if err != nil {
return err
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -41,45 +41,53 @@ type DcrdRPC struct {
}
type DcrdConnect struct {
*client
client *client
params *chaincfg.Params
}
func SetupDcrd(user, pass, addr string, cert []byte, n wsrpc.Notifier) DcrdConnect {
return DcrdConnect{setup(user, pass, addr, cert, n)}
func SetupDcrd(user, pass, addr string, cert []byte, n wsrpc.Notifier, params *chaincfg.Params) DcrdConnect {
return DcrdConnect{
client: setup(user, pass, addr, cert, n),
params: params,
}
}
func (d *DcrdConnect) Close() {
d.client.Close()
}
// Client creates a new DcrdRPC client instance. Returns an error if dialing
// dcrd fails or if dcrd is misconfigured.
func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, string, error) {
c, newConnection, err := d.dial(ctx)
func (d *DcrdConnect) Client(ctx context.Context) (*DcrdRPC, string, error) {
c, newConnection, err := d.client.dial(ctx)
if err != nil {
return nil, d.addr, fmt.Errorf("dcrd connection error: %w", err)
return nil, d.client.addr, fmt.Errorf("dcrd connection error: %w", err)
}
// If this is a reused connection, we don't need to validate the dcrd config
// again.
if !newConnection {
return &DcrdRPC{c, ctx}, d.addr, nil
return &DcrdRPC{c, ctx}, d.client.addr, nil
}
// Verify dcrd is at the required api version.
var verMap map[string]dcrdtypes.VersionResult
err = c.Call(ctx, "version", &verMap)
if err != nil {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd version check failed: %w", err)
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd version check failed: %w", err)
}
ver, exists := verMap["dcrdjsonrpcapi"]
if !exists {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'")
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'")
}
sVer := semver{ver.Major, ver.Minor, ver.Patch}
if !semverCompatible(requiredDcrdVersion, sVer) {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s",
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s",
sVer, requiredDcrdVersion)
}
@ -87,27 +95,27 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*
var netID wire.CurrencyNet
err = c.Call(ctx, "getcurrentnet", &netID)
if err != nil {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err)
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err)
}
if netID != netParams.Net {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net)
if netID != d.params.Net {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd running on %s, expected %s", netID, d.params.Net)
}
// Verify dcrd has tx index enabled (required for getrawtransaction).
var info dcrdtypes.InfoChainResult
err = c.Call(ctx, "getinfo", &info)
if err != nil {
d.Close()
return nil, d.addr, fmt.Errorf("dcrd getinfo check failed: %w", err)
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd getinfo check failed: %w", err)
}
if !info.TxIndex {
d.Close()
return nil, d.addr, errors.New("dcrd does not have transaction index enabled (--txindex)")
d.client.Close()
return nil, d.client.addr, errors.New("dcrd does not have transaction index enabled (--txindex)")
}
return &DcrdRPC{c, ctx}, d.addr, nil
return &DcrdRPC{c, ctx}, d.client.addr, nil
}
// GetRawTransaction uses getrawtransaction RPC to retrieve details about the

View File

@ -25,32 +25,38 @@ type WalletRPC struct {
ctx context.Context
}
type WalletConnect []*client
type WalletConnect struct {
clients []*client
params *chaincfg.Params
}
func SetupWallet(user, pass, addrs []string, cert [][]byte) WalletConnect {
walletConnect := make(WalletConnect, len(addrs))
func SetupWallet(user, pass, addrs []string, cert [][]byte, params *chaincfg.Params) WalletConnect {
clients := make([]*client, len(addrs))
for i := 0; i < len(addrs); i++ {
walletConnect[i] = setup(user[i], pass[i], addrs[i], cert[i], nil)
clients[i] = setup(user[i], pass[i], addrs[i], cert[i], nil)
}
return walletConnect
return WalletConnect{
clients: clients,
params: params,
}
}
func (w *WalletConnect) Close() {
for _, connect := range []*client(*w) {
connect.Close()
for _, client := range w.clients {
client.Close()
}
}
// Clients loops over each wallet and tries to establish a connection. It
// increments a count of failed connections if a connection cannot be
// established, or if the wallet is misconfigured.
func (w *WalletConnect) Clients(ctx context.Context, netParams *chaincfg.Params) ([]*WalletRPC, []string) {
func (w *WalletConnect) Clients(ctx context.Context) ([]*WalletRPC, []string) {
walletClients := make([]*WalletRPC, 0)
failedConnections := make([]string, 0)
for _, connect := range []*client(*w) {
for _, connect := range w.clients {
c, newConnection, err := connect.dial(ctx)
if err != nil {
@ -103,9 +109,9 @@ func (w *WalletConnect) Clients(ctx context.Context, netParams *chaincfg.Params)
connect.Close()
continue
}
if netID != netParams.Net {
if netID != w.params.Net {
log.Errorf("dcrwallet on wrong network (wallet=%s): running on %s, expected %s",
c.String(), netID, netParams.Net)
c.String(), netID, w.params.Net)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue

12
vspd.go
View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -84,15 +84,15 @@ func run(ctx context.Context) error {
// Create RPC client for local dcrd instance (used for broadcasting and
// checking the status of fee transactions).
dcrd := rpc.SetupDcrd(cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert, nil)
dcrd := rpc.SetupDcrd(cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert, nil, cfg.netParams.Params)
defer dcrd.Close()
// Create RPC client for remote dcrwallet instance (used for voting).
wallets := rpc.SetupWallet(cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts)
wallets := rpc.SetupWallet(cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts, cfg.netParams.Params)
defer wallets.Close()
// Ensure all data in database is present and up-to-date.
err = db.CheckIntegrity(ctx, cfg.netParams.Params, dcrd)
err = db.CheckIntegrity(ctx, dcrd)
if err != nil {
// vspd should still start if this fails, so just log an error.
log.Errorf("Could not check database integrity: %v", err)
@ -124,12 +124,12 @@ func run(ctx context.Context) error {
// Create a dcrd client with a blockconnected notification handler.
notifHandler := background.NotificationHandler{ShutdownWg: &shutdownWg}
dcrdWithNotifs := rpc.SetupDcrd(cfg.DcrdUser, cfg.DcrdPass,
cfg.DcrdHost, cfg.dcrdCert, &notifHandler)
cfg.DcrdHost, cfg.dcrdCert, &notifHandler, cfg.netParams.Params)
defer dcrdWithNotifs.Close()
// Start background process which will continually attempt to reconnect to
// dcrd if the connection drops.
background.Start(ctx, &shutdownWg, db, dcrd, dcrdWithNotifs, wallets, cfg.netParams.Params)
background.Start(ctx, &shutdownWg, db, dcrd, dcrdWithNotifs, wallets)
// Wait for shutdown tasks to complete before running deferred tasks and
// returning.

View File

@ -143,7 +143,7 @@ func (s *Server) statusJSON(c *gin.Context) {
// adminPage is the handler for "GET /admin".
func (s *Server) adminPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin.html", gin.H{
"WebApiCache": getCache(),
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
@ -185,7 +185,7 @@ func (s *Server) ticketSearch(c *gin.Context) {
VoteChanges: voteChanges,
MaxVoteChanges: s.cfg.MaxVoteChangeRecords,
},
"WebApiCache": getCache(),
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
@ -200,7 +200,7 @@ func (s *Server) adminLogin(c *gin.Context) {
if password != s.cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
"IncorrectPassword": true,
})

View File

@ -10,15 +10,21 @@ import (
"sync"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
"github.com/dustin/go-humanize"
)
// apiCache is used to cache values which are commonly used by the API, so
// cache is used to store values which are commonly used by the API, so
// repeated web requests don't repeatedly trigger DB or RPC calls.
type apiCache struct {
type cache struct {
// data is the cached data.
data cacheData
// mtx must be held to read/write cache data.
mtx sync.RWMutex
}
type cacheData struct {
UpdateTime string
PubKey string
DatabaseSize string
@ -32,31 +38,26 @@ type apiCache struct {
RevokedProportion float32
}
var cacheMtx sync.RWMutex
var cache apiCache
func (c *cache) getData() cacheData {
c.mtx.RLock()
defer c.mtx.RUnlock()
func getCache() apiCache {
cacheMtx.RLock()
defer cacheMtx.RUnlock()
return cache
return c.data
}
// initCache creates the struct which holds the cached VSP stats, and
// initializes it with static values.
func initCache(signPubKey string) {
cacheMtx.Lock()
defer cacheMtx.Unlock()
cache = apiCache{
PubKey: signPubKey,
// newCache creates a new cache and initializes it with static values.
func newCache(signPubKey string) *cache {
return &cache{
data: cacheData{
PubKey: signPubKey,
},
}
}
// updateCache updates the dynamic values in the cache (ticket counts and best
// block height).
func updateCache(ctx context.Context, db *database.VspDatabase,
dcrd rpc.DcrdConnect, netParams *chaincfg.Params, wallets rpc.WalletConnect) error {
// update will use the provided database and RPC connections to update the
// dynamic values in the cache.
func (c *cache) update(ctx context.Context, db *database.VspDatabase,
dcrd rpc.DcrdConnect, wallets rpc.WalletConnect) error {
dbSize, err := db.Size()
if err != nil {
@ -70,7 +71,7 @@ func updateCache(ctx context.Context, db *database.VspDatabase,
}
// Get latest best block height.
dcrdClient, _, err := dcrd.Client(ctx, netParams)
dcrdClient, _, err := dcrd.Client(ctx)
if err != nil {
return err
}
@ -84,7 +85,7 @@ func updateCache(ctx context.Context, db *database.VspDatabase,
return errors.New("dcr node reports a network ticket pool size of zero")
}
clients, failedConnections := wallets.Clients(ctx, netParams)
clients, failedConnections := wallets.Clients(ctx)
if len(clients) == 0 {
log.Error("Could not connect to any wallets")
} else if len(failedConnections) > 0 {
@ -92,29 +93,29 @@ func updateCache(ctx context.Context, db *database.VspDatabase,
len(failedConnections), len(clients))
}
cacheMtx.Lock()
defer cacheMtx.Unlock()
c.mtx.Lock()
defer c.mtx.Unlock()
cache.UpdateTime = dateTime(time.Now().Unix())
cache.DatabaseSize = humanize.Bytes(dbSize)
cache.Voting = voting
cache.Voted = voted
cache.TotalVotingWallets = int64(len(clients) + len(failedConnections))
cache.VotingWalletsOnline = int64(len(clients))
cache.Revoked = revoked
cache.BlockHeight = bestBlock.Height
cache.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
c.data.UpdateTime = dateTime(time.Now().Unix())
c.data.DatabaseSize = humanize.Bytes(dbSize)
c.data.Voting = voting
c.data.Voted = voted
c.data.TotalVotingWallets = int64(len(clients) + len(failedConnections))
c.data.VotingWalletsOnline = int64(len(clients))
c.data.Revoked = revoked
c.data.BlockHeight = bestBlock.Height
c.data.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
// Prevent dividing by zero when pool has no voted tickets.
switch voted {
case 0:
if revoked == 0 {
cache.RevokedProportion = 0
c.data.RevokedProportion = 0
} else {
cache.RevokedProportion = 1
c.data.RevokedProportion = 1
}
default:
cache.RevokedProportion = float32(revoked) / float32(voted)
c.data.RevokedProportion = float32(revoked) / float32(voted)
}
return nil

View File

@ -12,7 +12,7 @@ import (
func (s *Server) homepage(c *gin.Context) {
c.HTML(http.StatusOK, "homepage.html", gin.H{
"WebApiCache": getCache(),
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
})
}

View File

@ -59,7 +59,7 @@ func (s *Server) requireAdmin(c *gin.Context) {
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
"WebApiCache": s.cache.getData(),
"WebApiCfg": s.cfg,
})
c.Abort()
@ -69,9 +69,9 @@ func (s *Server) requireAdmin(c *gin.Context) {
// withDcrdClient middleware adds a dcrd client to the request context for
// downstream handlers to make use of.
func withDcrdClient(dcrd rpc.DcrdConnect, cfg Config) gin.HandlerFunc {
func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc {
return func(c *gin.Context) {
client, hostname, err := dcrd.Client(c, cfg.NetParams)
client, hostname, err := dcrd.Client(c)
// Don't handle the error here, add it to the context and let downstream
// handlers decide what to do with it.
c.Set(dcrdKey, client)
@ -83,9 +83,9 @@ func withDcrdClient(dcrd rpc.DcrdConnect, cfg Config) gin.HandlerFunc {
// withWalletClients middleware attempts to add voting wallet clients to the
// request context for downstream handlers to make use of. Downstream handlers
// must handle the case where no wallet clients are connected.
func withWalletClients(wallets rpc.WalletConnect, cfg Config) gin.HandlerFunc {
func withWalletClients(wallets rpc.WalletConnect) gin.HandlerFunc {
return func(c *gin.Context) {
clients, failedConnections := wallets.Clients(c, cfg.NetParams)
clients, failedConnections := wallets.Clients(c)
if len(clients) == 0 {
log.Error("Could not connect to any wallets")
} else if len(failedConnections) > 0 {

View File

@ -13,7 +13,7 @@ import (
// vspInfo is the handler for "GET /api/v3/vspinfo".
func (s *Server) vspInfo(c *gin.Context) {
cachedStats := getCache()
cachedStats := s.cache.getData()
s.sendJSONResponse(vspInfoResponse{
APIVersions: []int64{3},
Timestamp: time.Now().Unix(),

View File

@ -67,6 +67,7 @@ type Server struct {
cfg Config
db *database.VspDatabase
addrGen *addressGenerator
cache *cache
signPrivKey ed25519.PrivateKey
signPubKey ed25519.PublicKey
}
@ -88,8 +89,8 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
}
// Populate cached VSP stats before starting webserver.
initCache(base64.StdEncoding.EncodeToString(s.signPubKey))
err = updateCache(ctx, vdb, dcrd, config.NetParams, wallets)
s.cache = newCache(base64.StdEncoding.EncodeToString(s.signPubKey))
err = s.cache.update(ctx, vdb, dcrd, wallets)
if err != nil {
log.Errorf("Could not initialize VSP stats cache: %v", err)
}
@ -174,7 +175,7 @@ func Start(ctx context.Context, requestShutdown func(), shutdownWg *sync.WaitGro
shutdownWg.Done()
return
case <-time.After(refresh):
err := updateCache(ctx, vdb, dcrd, config.NetParams, wallets)
err := s.cache.update(ctx, vdb, dcrd, wallets)
if err != nil {
log.Errorf("Failed to update cached VSP stats: %v", err)
}
@ -230,11 +231,11 @@ func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
api := router.Group("/api/v3")
api.GET("/vspinfo", s.vspInfo)
api.POST("/setaltsignaddr", withDcrdClient(dcrd, s.cfg), s.broadcastTicket, s.vspAuth, s.setAltSignAddr)
api.POST("/feeaddress", withDcrdClient(dcrd, s.cfg), s.broadcastTicket, s.vspAuth, s.feeAddress)
api.POST("/ticketstatus", withDcrdClient(dcrd, s.cfg), s.vspAuth, s.ticketStatus)
api.POST("/payfee", withDcrdClient(dcrd, s.cfg), s.vspAuth, s.payFee)
api.POST("/setvotechoices", withDcrdClient(dcrd, s.cfg), withWalletClients(wallets, s.cfg), s.vspAuth, s.setVoteChoices)
api.POST("/setaltsignaddr", withDcrdClient(dcrd), s.broadcastTicket, s.vspAuth, s.setAltSignAddr)
api.POST("/feeaddress", withDcrdClient(dcrd), s.broadcastTicket, s.vspAuth, s.feeAddress)
api.POST("/ticketstatus", withDcrdClient(dcrd), s.vspAuth, s.ticketStatus)
api.POST("/payfee", withDcrdClient(dcrd), s.vspAuth, s.payFee)
api.POST("/setvotechoices", withDcrdClient(dcrd), withWalletClients(wallets), s.vspAuth, s.setVoteChoices)
// Website routes.
@ -246,16 +247,16 @@ func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
login.POST("", s.adminLogin)
admin := router.Group("/admin").Use(
withWalletClients(wallets, s.cfg), withSession(cookieStore), s.requireAdmin,
withWalletClients(wallets), withSession(cookieStore), s.requireAdmin,
)
admin.GET("", withDcrdClient(dcrd, s.cfg), s.adminPage)
admin.POST("/ticket", withDcrdClient(dcrd, s.cfg), s.ticketSearch)
admin.GET("", withDcrdClient(dcrd), s.adminPage)
admin.POST("/ticket", withDcrdClient(dcrd), s.ticketSearch)
admin.GET("/backup", s.downloadDatabaseBackup)
admin.POST("/logout", s.adminLogout)
// Require Basic HTTP Auth on /admin/status endpoint.
basic := router.Group("/admin").Use(
withDcrdClient(dcrd, s.cfg), withWalletClients(wallets, s.cfg), gin.BasicAuth(gin.Accounts{
withDcrdClient(dcrd), withWalletClients(wallets), gin.BasicAuth(gin.Accounts{
"admin": s.cfg.AdminPass,
}),
)