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
}
// 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
type Address struct {
Paging
@ -342,6 +356,7 @@ type Address struct {
ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"`
Erc20Contract *bchain.ContractInfo `json:"erc20Contract,omitempty"` // deprecated
AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"`
StakingPools []StakingPool `json:"stakingPools,omitempty"`
// helpers for explorer
Filter string `json:"-"`
XPubAddresses map[string]struct{} `json:"-"`
@ -504,6 +519,7 @@ type BlockbookInfo struct {
CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime,omitempty"`
HistoricalFiatRatesTime *time.Time `json:"historicalFiatRatesTime,omitempty"`
HistoricalTokenFiatRatesTime *time.Time `json:"historicalTokenFiatRatesTime,omitempty"`
SupportedStakingPools []string `json:"supportedStakingPools,omitempty"`
DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"`
DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"`
About string `json:"about"`

View File

@ -1058,6 +1058,7 @@ type ethereumTypeAddressData struct {
totalResults int
tokensBaseValue float64
tokensSecondaryValue float64
stakingPools []StakingPool
}
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
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
}
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) {
var tx *Tx
var err error
@ -1401,6 +1435,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
ContractInfo: ed.contractInfo,
Nonce: ed.nonce,
AddressAliases: w.getAddressAliases(addresses),
StakingPools: ed.stakingPools,
}
// keep address backward compatible, set deprecated Erc20Contract value if ERC20 token
if ed.contractInfo != nil && ed.contractInfo.Type == bchain.ERC20TokenType {
@ -2379,6 +2414,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
CurrentFiatRatesTime: nonZeroTime(currentFiatRatesTime),
HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime),
HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime),
SupportedStakingPools: w.chain.EthereumTypeGetSupportedStakingPools(),
DbSize: w.db.DatabaseSizeOnDisk(),
DbSizeFromColumns: internalDBSize,
DbColumns: columnStats,

View File

@ -41,30 +41,38 @@ func (b *BaseChain) GetMempoolEntry(txid string) (*MempoolEntry, error) {
// EthereumTypeGetBalance is not supported
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
func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) {
return 0, errors.New("Not supported")
return 0, errors.New("not supported")
}
// EthereumTypeEstimateGas is not supported
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
func (b *BaseChain) GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error) {
return nil, errors.New("Not supported")
return nil, errors.New("not supported")
}
// EthereumTypeGetErc20ContractBalance is not supported
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
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)
}
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 {
mempool bchain.Mempool
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
func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
addr := hexutil.Encode(addrDesc)
addr := hexutil.Encode(addrDesc)[2:]
contract := hexutil.Encode(contractDesc)
req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:]
req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr
data, err := b.ethCall(req, contract)
if err != nil {
return nil, err

View File

@ -56,23 +56,26 @@ type Configuration struct {
// EthereumRPC is an interface to JSON-RPC eth service.
type EthereumRPC struct {
*bchain.BaseChain
Client bchain.EVMClient
RPC bchain.EVMRPCClient
MainNetChainID Network
Timeout time.Duration
Parser *EthereumParser
PushHandler func(bchain.NotificationType)
OpenRPC func(string) (bchain.EVMRPCClient, bchain.EVMClient, error)
Mempool *bchain.MempoolEthereumType
mempoolInitialized bool
bestHeaderLock sync.Mutex
bestHeader bchain.EVMHeader
bestHeaderTime time.Time
NewBlock bchain.EVMNewBlockSubscriber
newBlockSubscription bchain.EVMClientSubscription
NewTx bchain.EVMNewTxSubscriber
newTxSubscription bchain.EVMClientSubscription
ChainConfig *Configuration
Client bchain.EVMClient
RPC bchain.EVMRPCClient
MainNetChainID Network
Timeout time.Duration
Parser *EthereumParser
PushHandler func(bchain.NotificationType)
OpenRPC func(string) (bchain.EVMRPCClient, bchain.EVMClient, error)
Mempool *bchain.MempoolEthereumType
mempoolInitialized bool
bestHeaderLock sync.Mutex
bestHeader bchain.EVMHeader
bestHeaderTime time.Time
NewBlock bchain.EVMNewBlockSubscriber
newBlockSubscription bchain.EVMClientSubscription
NewTx bchain.EVMNewTxSubscriber
newTxSubscription bchain.EVMClientSubscription
ChainConfig *Configuration
supportedStakingPools []string
stakingPoolNames []string
stakingPoolContracts []string
}
// ProcessInternalTransactions specifies if internal transactions are processed
@ -155,6 +158,12 @@ func (b *EthereumRPC) Initialize() error {
default:
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)
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)
EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error)
EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error)
EthereumTypeGetSupportedStakingPools() []string
EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error)
GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error)
}

View File

@ -146,3 +146,17 @@ type EthereumBlockSpecificData struct {
AddressAliasRecords []AddressAliasRecord
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;
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 {
type: string;
contract: string;
@ -161,6 +172,7 @@ export interface Address {
contractInfo?: ContractInfo;
erc20Contract?: ContractInfo;
addressAliases?: { [key: string]: AddressAlias };
stakingPools?: StakingPool[];
}
export interface Utxo {
txid: string;
@ -264,6 +276,7 @@ export interface BlockbookInfo {
currentFiatRatesTime?: string;
historicalFiatRatesTime?: string;
historicalTokenFiatRatesTime?: string;
stakingPoolContracts?: string[];
dbSizeFromColumns?: number;
dbColumns?: InternalStateColumn[];
about: string;

View File

@ -221,6 +221,64 @@
</div>
</div>
{{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}}
{{if or $addr.Transactions $addr.Filter}}
<div class="row pt-3 pb-1">

View File

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