diff --git a/api/types.go b/api/types.go index 91cc10a8..215c5881 100644 --- a/api/types.go +++ b/api/types.go @@ -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"` diff --git a/api/worker.go b/api/worker.go index de4968bb..a30dc06e 100644 --- a/api/worker.go +++ b/api/worker.go @@ -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, diff --git a/bchain/basechain.go b/bchain/basechain.go index bcd03d19..cecd2954 100644 --- a/bchain/basechain.go +++ b/bchain/basechain.go @@ -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") } diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index a017190f..8428b7d7 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -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 diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index 6405c9c8..34916346 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -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 diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index edf69ea4..8278219d 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -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 diff --git a/bchain/coins/eth/stakingpool.go b/bchain/coins/eth/stakingpool.go new file mode 100644 index 00000000..1947c77e --- /dev/null +++ b/bchain/coins/eth/stakingpool.go @@ -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 '/', 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 +} diff --git a/bchain/types.go b/bchain/types.go index c4f65657..a27431ab 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -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) } diff --git a/bchain/types_ethereum_type.go b/bchain/types_ethereum_type.go index 2a8ef6e0..9fc43d99 100644 --- a/bchain/types_ethereum_type.go +++ b/bchain/types_ethereum_type.go @@ -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 +} diff --git a/blockbook-api.ts b/blockbook-api.ts index 45d6bcab..09f064b1 100644 --- a/blockbook-api.ts +++ b/blockbook-api.ts @@ -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; diff --git a/static/templates/address.html b/static/templates/address.html index 21a3e1e3..a0e3dfaf 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -221,6 +221,64 @@ {{end}} +{{if $addr.StakingPools }} +
+
+
+ +
+
+
+ {{range $sp := $addr.StakingPools}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$sp.Name}} {{$sp.Contract}}
Pending Balance{{amountSpan $sp.PendingBalance $data "copyable"}}
Pending Deposited Balance{{amountSpan $sp.PendingDepositedBalance $data "copyable"}}
Deposited Balance{{amountSpan $sp.DepositedBalance $data "copyable"}}
Withdrawal Total Amount{{amountSpan $sp.WithdrawTotalAmount $data "copyable"}}
Claimable Amount{{amountSpan $sp.ClaimableAmount $data "copyable"}}
Pending Restaked Reward{{amountSpan $sp.PendingRestakedReward $data "copyable"}}
Restaked Reward{{amountSpan $sp.RestakedReward $data "copyable"}}
Autocompound Balance{{amountSpan $sp.AutocompoundBalance $data "copyable"}}
+ {{end}} +
+
+
+
+{{end}} {{end}} {{if or $addr.Transactions $addr.Filter}}
diff --git a/static/templates/index.html b/static/templates/index.html index 36d81fe5..3454943f 100644 --- a/static/templates/index.html +++ b/static/templates/index.html @@ -64,6 +64,12 @@ Size On Disk {{formatInt64 $bb.DbSize}} + {{if $bb.SupportedStakingPools}} + + Supported Staking Pools + {{$bb.SupportedStakingPools}} + + {{end}}