Add support of staking pools

This commit is contained in:
Martin Boehm 2024-01-19 01:26:46 +01:00
parent f03c625def
commit ac46385f49
12 changed files with 346 additions and 25 deletions

View File

@ -316,6 +316,20 @@ type AddressFilter struct {
OnlyConfirmed bool OnlyConfirmed bool
} }
// StakingPool holds data about address participation in a staking pool contract
type StakingPool struct {
Contract string `json:"contract"`
Name string `json:"name"`
PendingBalance *Amount `json:"pendingBalance"`
PendingDepositedBalance *Amount `json:"pendingDepositedBalance"`
DepositedBalance *Amount `json:"depositedBalance"`
WithdrawTotalAmount *Amount `json:"withdrawTotalAmount"`
ClaimableAmount *Amount `json:"claimableAmount"`
PendingRestakedReward *Amount `json:"pendingRestakedReward"`
RestakedReward *Amount `json:"restakedReward"`
AutocompoundBalance *Amount `json:"autocompoundBalance"`
}
// Address holds information about address and its transactions // Address holds information about address and its transactions
type Address struct { type Address struct {
Paging Paging
@ -342,6 +356,7 @@ type Address struct {
ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"` ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"`
Erc20Contract *bchain.ContractInfo `json:"erc20Contract,omitempty"` // deprecated Erc20Contract *bchain.ContractInfo `json:"erc20Contract,omitempty"` // deprecated
AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"`
StakingPools []StakingPool `json:"stakingPools,omitempty"`
// helpers for explorer // helpers for explorer
Filter string `json:"-"` Filter string `json:"-"`
XPubAddresses map[string]struct{} `json:"-"` XPubAddresses map[string]struct{} `json:"-"`
@ -504,6 +519,7 @@ type BlockbookInfo struct {
CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime,omitempty"` CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime,omitempty"`
HistoricalFiatRatesTime *time.Time `json:"historicalFiatRatesTime,omitempty"` HistoricalFiatRatesTime *time.Time `json:"historicalFiatRatesTime,omitempty"`
HistoricalTokenFiatRatesTime *time.Time `json:"historicalTokenFiatRatesTime,omitempty"` HistoricalTokenFiatRatesTime *time.Time `json:"historicalTokenFiatRatesTime,omitempty"`
SupportedStakingPools []string `json:"supportedStakingPools,omitempty"`
DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"` DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"`
DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"` DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"`
About string `json:"about"` About string `json:"about"`

View File

@ -1058,6 +1058,7 @@ type ethereumTypeAddressData struct {
totalResults int totalResults int
tokensBaseValue float64 tokensBaseValue float64
tokensSecondaryValue float64 tokensSecondaryValue float64
stakingPools []StakingPool
} }
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter, secondaryCoin string) (*db.AddrBalance, *ethereumTypeAddressData, error) { func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter, secondaryCoin string) (*db.AddrBalance, *ethereumTypeAddressData, error) {
@ -1157,9 +1158,42 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
filter.Vout = AddressFilterVoutQueryNotNecessary filter.Vout = AddressFilterVoutQueryNotNecessary
d.totalResults = -1 d.totalResults = -1
} }
// if staking pool enabled, fetch the staking pool details
if details >= AccountDetailsTokenBalances {
d.stakingPools, err = w.getStakingPoolsData(addrDesc)
if err != nil {
return nil, nil, err
}
}
return ba, &d, nil return ba, &d, nil
} }
func (w *Worker) getStakingPoolsData(addrDesc bchain.AddressDescriptor) ([]StakingPool, error) {
var pools []StakingPool
if len(w.chain.EthereumTypeGetSupportedStakingPools()) > 0 {
sp, err := w.chain.EthereumTypeGetStakingPoolsData(addrDesc)
if err != nil {
return nil, err
}
for i := range sp {
p := &sp[i]
pools = append(pools, StakingPool{
Contract: p.Contract,
Name: p.Name,
PendingBalance: (*Amount)(&p.PendingBalance),
PendingDepositedBalance: (*Amount)(&p.PendingDepositedBalance),
DepositedBalance: (*Amount)(&p.DepositedBalance),
WithdrawTotalAmount: (*Amount)(&p.WithdrawTotalAmount),
ClaimableAmount: (*Amount)(&p.ClaimableAmount),
PendingRestakedReward: (*Amount)(&p.PendingRestakedReward),
RestakedReward: (*Amount)(&p.RestakedReward),
AutocompoundBalance: (*Amount)(&p.AutocompoundBalance),
})
}
}
return pools, nil
}
func (w *Worker) txFromTxid(txid string, bestHeight uint32, option AccountDetails, blockInfo *db.BlockInfo, addresses map[string]struct{}) (*Tx, error) { func (w *Worker) txFromTxid(txid string, bestHeight uint32, option AccountDetails, blockInfo *db.BlockInfo, addresses map[string]struct{}) (*Tx, error) {
var tx *Tx var tx *Tx
var err error var err error
@ -1401,6 +1435,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
ContractInfo: ed.contractInfo, ContractInfo: ed.contractInfo,
Nonce: ed.nonce, Nonce: ed.nonce,
AddressAliases: w.getAddressAliases(addresses), AddressAliases: w.getAddressAliases(addresses),
StakingPools: ed.stakingPools,
} }
// keep address backward compatible, set deprecated Erc20Contract value if ERC20 token // keep address backward compatible, set deprecated Erc20Contract value if ERC20 token
if ed.contractInfo != nil && ed.contractInfo.Type == bchain.ERC20TokenType { if ed.contractInfo != nil && ed.contractInfo.Type == bchain.ERC20TokenType {
@ -2379,6 +2414,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
CurrentFiatRatesTime: nonZeroTime(currentFiatRatesTime), CurrentFiatRatesTime: nonZeroTime(currentFiatRatesTime),
HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime), HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime),
HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime), HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime),
SupportedStakingPools: w.chain.EthereumTypeGetSupportedStakingPools(),
DbSize: w.db.DatabaseSizeOnDisk(), DbSize: w.db.DatabaseSizeOnDisk(),
DbSizeFromColumns: internalDBSize, DbSizeFromColumns: internalDBSize,
DbColumns: columnStats, DbColumns: columnStats,

View File

@ -41,30 +41,38 @@ func (b *BaseChain) GetMempoolEntry(txid string) (*MempoolEntry, error) {
// EthereumTypeGetBalance is not supported // EthereumTypeGetBalance is not supported
func (b *BaseChain) EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) { func (b *BaseChain) EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("Not supported") return nil, errors.New("not supported")
} }
// EthereumTypeGetNonce is not supported // EthereumTypeGetNonce is not supported
func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) { func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) {
return 0, errors.New("Not supported") return 0, errors.New("not supported")
} }
// EthereumTypeEstimateGas is not supported // EthereumTypeEstimateGas is not supported
func (b *BaseChain) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { func (b *BaseChain) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) {
return 0, errors.New("Not supported") return 0, errors.New("not supported")
} }
// GetContractInfo is not supported // GetContractInfo is not supported
func (b *BaseChain) GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error) { func (b *BaseChain) GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error) {
return nil, errors.New("Not supported") return nil, errors.New("not supported")
} }
// EthereumTypeGetErc20ContractBalance is not supported // EthereumTypeGetErc20ContractBalance is not supported
func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) { func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("Not supported") return nil, errors.New("not supported")
} }
// GetContractInfo returns URI of non fungible or multi token defined by token id // GetContractInfo returns URI of non fungible or multi token defined by token id
func (p *BaseChain) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) { func (p *BaseChain) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) {
return "", errors.New("Not supported") return "", errors.New("not supported")
}
func (b *BaseChain) EthereumTypeGetSupportedStakingPools() []string {
return nil
}
func (b *BaseChain) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) {
return nil, errors.New("not supported")
} }

View File

@ -332,6 +332,15 @@ func (c *blockChainWithMetrics) GetTokenURI(contractDesc bchain.AddressDescripto
return c.b.GetTokenURI(contractDesc, tokenID) return c.b.GetTokenURI(contractDesc, tokenID)
} }
func (c *blockChainWithMetrics) EthereumTypeGetSupportedStakingPools() []string {
return c.b.EthereumTypeGetSupportedStakingPools()
}
func (c *blockChainWithMetrics) EthereumTypeGetStakingPoolsData(addrDesc bchain.AddressDescriptor) (v []bchain.StakingPoolData, err error) {
defer func(s time.Time) { c.observeRPCLatency("EthereumTypeStakingPoolsData", s, err) }(time.Now())
return c.b.EthereumTypeGetStakingPoolsData(addrDesc)
}
type mempoolWithMetrics struct { type mempoolWithMetrics struct {
mempool bchain.Mempool mempool bchain.Mempool
m *common.Metrics m *common.Metrics

View File

@ -337,9 +337,9 @@ func (b *EthereumRPC) GetContractInfo(contractDesc bchain.AddressDescriptor) (*b
// EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address // EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address
func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) { func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
addr := hexutil.Encode(addrDesc) addr := hexutil.Encode(addrDesc)[2:]
contract := hexutil.Encode(contractDesc) contract := hexutil.Encode(contractDesc)
req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:] req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr
data, err := b.ethCall(req, contract) data, err := b.ethCall(req, contract)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -73,6 +73,9 @@ type EthereumRPC struct {
NewTx bchain.EVMNewTxSubscriber NewTx bchain.EVMNewTxSubscriber
newTxSubscription bchain.EVMClientSubscription newTxSubscription bchain.EVMClientSubscription
ChainConfig *Configuration ChainConfig *Configuration
supportedStakingPools []string
stakingPoolNames []string
stakingPoolContracts []string
} }
// ProcessInternalTransactions specifies if internal transactions are processed // ProcessInternalTransactions specifies if internal transactions are processed
@ -155,6 +158,12 @@ func (b *EthereumRPC) Initialize() error {
default: default:
return errors.Errorf("Unknown network id %v", id) return errors.Errorf("Unknown network id %v", id)
} }
err = b.initStakingPools(b.ChainConfig.CoinShortcut)
if err != nil {
return err
}
glog.Info("rpc: block chain ", b.Network) glog.Info("rpc: block chain ", b.Network)
return nil return nil

View File

@ -0,0 +1,150 @@
package eth
import (
"math/big"
"os"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/glog"
"github.com/juju/errors"
"github.com/trezor/blockbook/bchain"
)
func (b *EthereumRPC) initStakingPools(coinShortcut string) error {
// for now only single staking pool
envVar := strings.ToUpper(coinShortcut) + "_STAKING_POOL_CONTRACT"
envValue := os.Getenv(envVar)
if envValue != "" {
parts := strings.Split(envValue, "/")
if len(parts) != 2 {
glog.Errorf("Wrong format of environment variable %s=%s, expecting value '<pool name>/<pool contract>', staking pools not enabled", envVar, envValue)
return nil
}
b.supportedStakingPools = []string{envValue}
b.stakingPoolNames = []string{parts[0]}
b.stakingPoolContracts = []string{parts[1]}
glog.Info("Support of staking pools enabled with these pools: ", b.supportedStakingPools)
}
return nil
}
func (b *EthereumRPC) EthereumTypeGetSupportedStakingPools() []string {
return b.supportedStakingPools
}
func (b *EthereumRPC) EthereumTypeGetStakingPoolsData(addrDesc bchain.AddressDescriptor) ([]bchain.StakingPoolData, error) {
// for now only single staking pool - Everstake
addr := hexutil.Encode(addrDesc)[2:]
if len(b.supportedStakingPools) == 1 {
data, err := b.everstakePoolData(addr, b.stakingPoolContracts[0], b.stakingPoolNames[0])
if err != nil {
return nil, err
}
if data != nil {
return []bchain.StakingPoolData{*data}, nil
}
}
return nil, nil
}
const everstakePendingBalanceOfMethodSignature = "0x59b8c763" // pendingBalanceOf(address)
const everstakePendingDepositedBalanceOfMethodSignature = "0x80f14ecc" // pendingDepositedBalanceOf(address)
const everstakeDepositedBalanceOfMethodSignature = "0x68b48254" // depositedBalanceOf(address)
const everstakeWithdrawRequestMethodSignature = "0x14cbc46a" // withdrawRequest(address)
const everstakePendingRestakedRewardOfMethodSignature = "0x376d1884" // pendingRestakedRewardOf(address)
const everstakeRestakedRewardOfMethodSignature = "0x0c98929a" // restakedRewardOf(address)
const everstakeAutocompoundBalanceOfMethodSignature = "0x2fec7966" // autocompoundBalanceOf(address)
func isZeroBigInt(b *big.Int) bool {
return len(b.Bits()) == 0
}
func (b *EthereumRPC) everstakeBalanceTypeContractCall(signature, addr, contract string) (string, error) {
req := signature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr
return b.ethCall(req, contract)
}
func (b *EthereumRPC) everstakeContractCallSimpleNumeric(signature, addr, contract string) (*big.Int, error) {
data, err := b.everstakeBalanceTypeContractCall(signature, addr, contract)
if err != nil {
return nil, err
}
r := parseSimpleNumericProperty(data)
if r == nil {
return nil, errors.New("Invalid balance")
}
return r, nil
}
func (b *EthereumRPC) everstakePoolData(addr, contract, name string) (*bchain.StakingPoolData, error) {
poolData := bchain.StakingPoolData{
Contract: contract,
Name: name,
}
allZeros := true
value, err := b.everstakeContractCallSimpleNumeric(everstakePendingBalanceOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.PendingBalance = *value
allZeros = allZeros && isZeroBigInt(value)
value, err = b.everstakeContractCallSimpleNumeric(everstakePendingDepositedBalanceOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.PendingDepositedBalance = *value
allZeros = allZeros && isZeroBigInt(value)
value, err = b.everstakeContractCallSimpleNumeric(everstakeDepositedBalanceOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.DepositedBalance = *value
allZeros = allZeros && isZeroBigInt(value)
data, err := b.everstakeBalanceTypeContractCall(everstakeWithdrawRequestMethodSignature, addr, contract)
if err != nil {
return nil, err
}
value = parseSimpleNumericProperty(data)
if value == nil {
return nil, errors.New("Invalid balance")
}
poolData.WithdrawTotalAmount = *value
allZeros = allZeros && isZeroBigInt(value)
value = parseSimpleNumericProperty(data[64+2:])
if value == nil {
return nil, errors.New("Invalid balance")
}
poolData.ClaimableAmount = *value
allZeros = allZeros && isZeroBigInt(value)
value, err = b.everstakeContractCallSimpleNumeric(everstakePendingRestakedRewardOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.PendingRestakedReward = *value
allZeros = allZeros && isZeroBigInt(value)
value, err = b.everstakeContractCallSimpleNumeric(everstakeRestakedRewardOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.RestakedReward = *value
allZeros = allZeros && isZeroBigInt(value)
value, err = b.everstakeContractCallSimpleNumeric(everstakeAutocompoundBalanceOfMethodSignature, addr, contract)
if err != nil {
return nil, err
}
poolData.AutocompoundBalance = *value
allZeros = allZeros && isZeroBigInt(value)
if allZeros {
return nil, nil
}
return &poolData, nil
}

View File

@ -333,6 +333,8 @@ type BlockChain interface {
EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error)
EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error)
EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error)
EthereumTypeGetSupportedStakingPools() []string
EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error)
GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error)
} }

View File

@ -146,3 +146,17 @@ type EthereumBlockSpecificData struct {
AddressAliasRecords []AddressAliasRecord AddressAliasRecords []AddressAliasRecord
Contracts []ContractInfo Contracts []ContractInfo
} }
// StakingPool holds data about address participation in a staking pool contract
type StakingPoolData struct {
Contract string `json:"contract"`
Name string `json:"name"`
PendingBalance big.Int `json:"pendingBalance"` // pendingBalanceOf method
PendingDepositedBalance big.Int `json:"pendingDepositedBalance"` // pendingDepositedBalanceOf method
DepositedBalance big.Int `json:"depositedBalance"` // depositedBalanceOf method
WithdrawTotalAmount big.Int `json:"withdrawTotalAmount"` // withdrawRequest method, return value [0]
ClaimableAmount big.Int `json:"claimableAmount"` // withdrawRequest method, return value [1]
PendingRestakedReward big.Int `json:"pendingRestakedReward"` // pendingRestakedRewardOf method
RestakedReward big.Int `json:"restakedReward"` // restakedRewardOf method
AutocompoundBalance big.Int `json:"autocompoundBalance"` // autocompoundBalanceOf method
}

View File

@ -109,6 +109,17 @@ export interface FeeStats {
averageFeePerKb: number; averageFeePerKb: number;
decilesFeePerKb: number[]; decilesFeePerKb: number[];
} }
export interface StakingPool {
contract: string;
pendingBalance: string;
pendingDepositedBalance: string;
depositedBalance: string;
withdrawTotalAmount: string;
claimableAmount: string;
pendingRestakedReward: string;
restakedReward: string;
autocompoundBalance: string;
}
export interface ContractInfo { export interface ContractInfo {
type: string; type: string;
contract: string; contract: string;
@ -161,6 +172,7 @@ export interface Address {
contractInfo?: ContractInfo; contractInfo?: ContractInfo;
erc20Contract?: ContractInfo; erc20Contract?: ContractInfo;
addressAliases?: { [key: string]: AddressAlias }; addressAliases?: { [key: string]: AddressAlias };
stakingPools?: StakingPool[];
} }
export interface Utxo { export interface Utxo {
txid: string; txid: string;
@ -264,6 +276,7 @@ export interface BlockbookInfo {
currentFiatRatesTime?: string; currentFiatRatesTime?: string;
historicalFiatRatesTime?: string; historicalFiatRatesTime?: string;
historicalTokenFiatRatesTime?: string; historicalTokenFiatRatesTime?: string;
stakingPoolContracts?: string[];
dbSizeFromColumns?: number; dbSizeFromColumns?: number;
dbColumns?: InternalStateColumn[]; dbColumns?: InternalStateColumn[];
about: string; about: string;

View File

@ -221,6 +221,64 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if $addr.StakingPools }}
<div class="accordion mt-2 mb-2" id="stakingPools">
<div class="accordion-item">
<div class="accordion-header" id="stakingPoolsHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#stakingPoolsBody" aria-expanded="false" aria-controls="stakingPoolsBody">
<div class="row g-0 w-100">
<h5 class="col-12 mb-md-0">Staking Pools <span class="badge bg-secondary">{{len $addr.StakingPools}}</span></span></h5>
</div>
</button>
</div>
<div id="stakingPoolsBody" class="accordion-collapse collapse" aria-labelledby="stakingPoolsHeading" data-bs-parent="#stakingPools">
<div class="accordion-body">
{{range $sp := $addr.StakingPools}}
<table class="table data-table info-table mt-0 mb-2 ml-0">
<tbody>
<tr>
<td colspan="2" style="white-space: nowrap;"><span class="h5" style="color: var(--bs-body-color);">{{$sp.Name}}</span> {{$sp.Contract}}</td>
</tr>
<tr>
<td style="width: 25%;">Pending Balance</td>
<td>{{amountSpan $sp.PendingBalance $data "copyable"}}</td>
</tr>
<tr>
<td>Pending Deposited Balance</td>
<td>{{amountSpan $sp.PendingDepositedBalance $data "copyable"}}</td>
</tr>
<tr>
<td style="width: 25%;">Deposited Balance</td>
<td>{{amountSpan $sp.DepositedBalance $data "copyable"}}</td>
</tr>
<tr>
<td>Withdrawal Total Amount</td>
<td>{{amountSpan $sp.WithdrawTotalAmount $data "copyable"}}</td>
</tr>
<tr>
<td style="width: 25%;">Claimable Amount</td>
<td>{{amountSpan $sp.ClaimableAmount $data "copyable"}}</td>
</tr>
<tr>
<td>Pending Restaked Reward</td>
<td>{{amountSpan $sp.PendingRestakedReward $data "copyable"}}</td>
</tr>
<tr>
<td>Restaked Reward</td>
<td>{{amountSpan $sp.RestakedReward $data "copyable"}}</td>
</tr>
<tr>
<td>Autocompound Balance</td>
<td>{{amountSpan $sp.AutocompoundBalance $data "copyable"}}</td>
</tr>
</tbody>
</table>
{{end}}
</div>
</div>
</div>
</div>
{{end}}
{{end}} {{end}}
{{if or $addr.Transactions $addr.Filter}} {{if or $addr.Transactions $addr.Filter}}
<div class="row pt-3 pb-1"> <div class="row pt-3 pb-1">

View File

@ -64,6 +64,12 @@
<td>Size On Disk</td> <td>Size On Disk</td>
<td>{{formatInt64 $bb.DbSize}}</td> <td>{{formatInt64 $bb.DbSize}}</td>
</tr> </tr>
{{if $bb.SupportedStakingPools}}
<tr>
<td>Supported Staking Pools</td>
<td>{{$bb.SupportedStakingPools}}</td>
</tr>
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>