-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[stakingindex] implement indexer for new staking contract (#4237)
- Loading branch information
Showing
9 changed files
with
1,608 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package abiutil | ||
|
||
import ( | ||
"fmt" | ||
"math/big" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/iotexproject/iotex-address/address" | ||
|
||
"github.com/iotexproject/iotex-core/action" | ||
) | ||
|
||
type ( | ||
// EventParam is a struct to hold smart contract event parameters, which can easily convert a param to go type | ||
EventParam struct { | ||
params []any | ||
nameToIndex map[string]int | ||
} | ||
) | ||
|
||
var ( | ||
// ErrInvlidEventParam is an error for invalid event param | ||
ErrInvlidEventParam = errors.New("invalid event param") | ||
) | ||
|
||
// EventField is a helper function to get a field from event param | ||
func EventField[T any](e EventParam, name string) (T, error) { | ||
id, ok := e.nameToIndex[name] | ||
if !ok { | ||
var zeroValue T | ||
return zeroValue, errors.Wrapf(ErrInvlidEventParam, "field %s not found", name) | ||
} | ||
return EventFieldByID[T](e, id) | ||
} | ||
|
||
// EventFieldByID is a helper function to get a field from event param | ||
func EventFieldByID[T any](e EventParam, id int) (T, error) { | ||
field, ok := e.fieldByID(id).(T) | ||
if !ok { | ||
return field, errors.Wrapf(ErrInvlidEventParam, "field %d got %#v, expect %T", id, e.fieldByID(id), field) | ||
} | ||
return field, nil | ||
} | ||
|
||
func (e EventParam) field(name string) any { | ||
return e.params[e.nameToIndex[name]] | ||
} | ||
|
||
func (e EventParam) fieldByID(id int) any { | ||
return e.params[id] | ||
} | ||
|
||
func (e EventParam) String() string { | ||
return fmt.Sprintf("%+v", e.params) | ||
} | ||
|
||
// FieldUint256 is a helper function to get a uint256 field from event param | ||
func (e EventParam) FieldUint256(name string) (*big.Int, error) { | ||
return EventField[*big.Int](e, name) | ||
} | ||
|
||
// FieldByIDUint256 is a helper function to get a uint256 field from event param | ||
func (e EventParam) FieldByIDUint256(id int) (*big.Int, error) { | ||
return EventFieldByID[*big.Int](e, id) | ||
} | ||
|
||
// FieldBytes12 is a helper function to get a bytes12 field from event param | ||
func (e EventParam) FieldBytes12(name string) (string, error) { | ||
id, ok := e.nameToIndex[name] | ||
if !ok { | ||
return "", errors.Wrapf(ErrInvlidEventParam, "field %s not found", name) | ||
} | ||
return e.FieldByIDBytes12(id) | ||
} | ||
|
||
// FieldByIDBytes12 is a helper function to get a bytes12 field from event param | ||
func (e EventParam) FieldByIDBytes12(id int) (string, error) { | ||
data, err := EventFieldByID[[12]byte](e, id) | ||
if err != nil { | ||
return "", err | ||
} | ||
// remove trailing zeros | ||
tail := len(data) - 1 | ||
for ; tail >= 0 && data[tail] == 0; tail-- { | ||
} | ||
return string(data[:tail+1]), nil | ||
} | ||
|
||
// FieldUint256Slice is a helper function to get a uint256 slice field from event param | ||
func (e EventParam) FieldUint256Slice(name string) ([]*big.Int, error) { | ||
return EventField[[]*big.Int](e, name) | ||
} | ||
|
||
// FieldByIDUint256Slice is a helper function to get a uint256 slice field from event param | ||
func (e EventParam) FieldByIDUint256Slice(id int) ([]*big.Int, error) { | ||
return EventFieldByID[[]*big.Int](e, id) | ||
} | ||
|
||
// FieldAddress is a helper function to get an address field from event param | ||
func (e EventParam) FieldAddress(name string) (address.Address, error) { | ||
commAddr, err := EventField[common.Address](e, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return address.FromBytes(commAddr.Bytes()) | ||
} | ||
|
||
// FieldByIDAddress is a helper function to get an address field from event param | ||
func (e EventParam) FieldByIDAddress(id int) (address.Address, error) { | ||
commAddr, err := EventFieldByID[common.Address](e, id) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return address.FromBytes(commAddr.Bytes()) | ||
} | ||
|
||
// UnpackEventParam is a helper function to unpack event parameters | ||
func UnpackEventParam(abiEvent *abi.Event, log *action.Log) (*EventParam, error) { | ||
// unpack non-indexed fields | ||
params := make(map[string]any) | ||
if len(log.Data) > 0 { | ||
if err := abiEvent.Inputs.UnpackIntoMap(params, log.Data); err != nil { | ||
return nil, errors.Wrap(err, "unpack event data failed") | ||
} | ||
} | ||
// unpack indexed fields | ||
args := make(abi.Arguments, 0) | ||
for _, arg := range abiEvent.Inputs { | ||
if arg.Indexed { | ||
args = append(args, arg) | ||
} | ||
} | ||
topics := make([]common.Hash, 0) | ||
for i, topic := range log.Topics { | ||
if i > 0 { | ||
topics = append(topics, common.Hash(topic)) | ||
} | ||
} | ||
err := abi.ParseTopicsIntoMap(params, args, topics) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "unpack event indexed fields failed") | ||
} | ||
// create event param | ||
event := &EventParam{ | ||
params: make([]any, 0, len(abiEvent.Inputs)), | ||
nameToIndex: make(map[string]int), | ||
} | ||
for i, arg := range abiEvent.Inputs { | ||
event.params = append(event.params, params[arg.Name]) | ||
event.nameToIndex[arg.Name] = i | ||
} | ||
return event, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package systemcontractindex | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"github.com/iotexproject/iotex-core/db" | ||
"github.com/iotexproject/iotex-core/db/batch" | ||
"github.com/iotexproject/iotex-core/pkg/util/byteutil" | ||
) | ||
|
||
// IndexerCommon is the common struct for all contract indexers | ||
// It provides the basic functions, including | ||
// 1. kvstore | ||
// 2. put/get index height | ||
// 3. contract address | ||
type IndexerCommon struct { | ||
kvstore db.KVStore | ||
ns string | ||
key []byte | ||
startHeight uint64 | ||
height uint64 | ||
contractAddress string | ||
} | ||
|
||
// NewIndexerCommon creates a new IndexerCommon | ||
func NewIndexerCommon(kvstore db.KVStore, ns string, key []byte, contractAddress string, startHeight uint64) *IndexerCommon { | ||
return &IndexerCommon{ | ||
kvstore: kvstore, | ||
ns: ns, | ||
key: key, | ||
startHeight: startHeight, | ||
contractAddress: contractAddress, | ||
} | ||
} | ||
|
||
// Start starts the indexer | ||
func (s *IndexerCommon) Start(ctx context.Context) error { | ||
if err := s.kvstore.Start(ctx); err != nil { | ||
return err | ||
} | ||
h, err := s.loadHeight() | ||
if err != nil { | ||
return err | ||
} | ||
s.height = h | ||
return nil | ||
} | ||
|
||
// Stop stops the indexer | ||
func (s *IndexerCommon) Stop(ctx context.Context) error { | ||
return s.kvstore.Stop(ctx) | ||
} | ||
|
||
// KVStore returns the kvstore | ||
func (s *IndexerCommon) KVStore() db.KVStore { return s.kvstore } | ||
|
||
// ContractAddress returns the contract address | ||
func (s *IndexerCommon) ContractAddress() string { return s.contractAddress } | ||
|
||
// Height returns the tip block height | ||
func (s *IndexerCommon) Height() uint64 { | ||
return s.height | ||
} | ||
|
||
func (s *IndexerCommon) loadHeight() (uint64, error) { | ||
// get the tip block height | ||
var height uint64 | ||
h, err := s.kvstore.Get(s.ns, s.key) | ||
if err != nil { | ||
if !errors.Is(err, db.ErrNotExist) { | ||
return 0, err | ||
} | ||
height = 0 | ||
} else { | ||
height = byteutil.BytesToUint64BigEndian(h) | ||
} | ||
return height, nil | ||
} | ||
|
||
// StartHeight returns the start height of the indexer | ||
func (s *IndexerCommon) StartHeight() uint64 { return s.startHeight } | ||
|
||
// Commit commits the height to the indexer | ||
func (s *IndexerCommon) Commit(height uint64, delta batch.KVStoreBatch) error { | ||
delta.Put(s.ns, s.key, byteutil.Uint64ToBytesBigEndian(height), "failed to put height") | ||
if err := s.kvstore.WriteBatch(delta); err != nil { | ||
return err | ||
} | ||
s.height = height | ||
return nil | ||
} | ||
|
||
// ExpectedHeight returns the expected height | ||
func (s *IndexerCommon) ExpectedHeight() uint64 { | ||
if s.height < s.startHeight { | ||
return s.startHeight | ||
} | ||
return s.height + 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package stakingindex | ||
|
||
import ( | ||
"math/big" | ||
"time" | ||
|
||
"github.com/iotexproject/iotex-address/address" | ||
"github.com/pkg/errors" | ||
"google.golang.org/protobuf/proto" | ||
|
||
"github.com/iotexproject/iotex-core/action/protocol/staking" | ||
"github.com/iotexproject/iotex-core/pkg/util/byteutil" | ||
"github.com/iotexproject/iotex-core/systemcontractindex/stakingindex/stakingpb" | ||
) | ||
|
||
type VoteBucket = staking.VoteBucket | ||
|
||
type Bucket struct { | ||
Candidate address.Address | ||
Owner address.Address | ||
StakedAmount *big.Int | ||
StakedDurationBlockNumber uint64 | ||
CreatedAt uint64 | ||
UnlockedAt uint64 | ||
UnstakedAt uint64 | ||
} | ||
|
||
func (bi *Bucket) Serialize() []byte { | ||
return byteutil.Must(proto.Marshal(bi.toProto())) | ||
} | ||
|
||
// Deserialize deserializes the bucket info | ||
func (bi *Bucket) Deserialize(b []byte) error { | ||
m := stakingpb.Bucket{} | ||
if err := proto.Unmarshal(b, &m); err != nil { | ||
return err | ||
} | ||
return bi.loadProto(&m) | ||
} | ||
|
||
// clone clones the bucket info | ||
func (bi *Bucket) toProto() *stakingpb.Bucket { | ||
return &stakingpb.Bucket{ | ||
Candidate: bi.Candidate.String(), | ||
CreatedAt: bi.CreatedAt, | ||
Owner: bi.Owner.String(), | ||
UnlockedAt: bi.UnlockedAt, | ||
UnstakedAt: bi.UnstakedAt, | ||
Amount: bi.StakedAmount.String(), | ||
Duration: bi.StakedDurationBlockNumber, | ||
} | ||
} | ||
|
||
func (bi *Bucket) loadProto(p *stakingpb.Bucket) error { | ||
candidate, err := address.FromString(p.Candidate) | ||
if err != nil { | ||
return err | ||
} | ||
owner, err := address.FromString(p.Owner) | ||
if err != nil { | ||
return err | ||
} | ||
amount, ok := new(big.Int).SetString(p.Amount, 10) | ||
if !ok { | ||
return errors.Errorf("invalid staked amount %s", p.Amount) | ||
} | ||
bi.CreatedAt = p.CreatedAt | ||
bi.UnlockedAt = p.UnlockedAt | ||
bi.UnstakedAt = p.UnstakedAt | ||
bi.Candidate = candidate | ||
bi.Owner = owner | ||
bi.StakedAmount = amount | ||
bi.StakedDurationBlockNumber = p.Duration | ||
return nil | ||
} | ||
|
||
func (b *Bucket) Clone() *Bucket { | ||
clone := &Bucket{ | ||
StakedDurationBlockNumber: b.StakedDurationBlockNumber, | ||
CreatedAt: b.CreatedAt, | ||
UnlockedAt: b.UnlockedAt, | ||
UnstakedAt: b.UnstakedAt, | ||
} | ||
candidate, _ := address.FromBytes(b.Candidate.Bytes()) | ||
clone.Candidate = candidate | ||
owner, _ := address.FromBytes(b.Owner.Bytes()) | ||
clone.Owner = owner | ||
clone.StakedAmount = new(big.Int).Set(b.StakedAmount) | ||
return clone | ||
} | ||
|
||
func assembleVoteBucket(token uint64, bkt *Bucket, contractAddr string, blockInterval time.Duration) *VoteBucket { | ||
vb := VoteBucket{ | ||
Index: token, | ||
StakedAmount: bkt.StakedAmount, | ||
StakedDuration: time.Duration(bkt.StakedDurationBlockNumber) * blockInterval, | ||
StakedDurationBlockNumber: bkt.StakedDurationBlockNumber, | ||
CreateBlockHeight: bkt.CreatedAt, | ||
StakeStartBlockHeight: bkt.CreatedAt, | ||
UnstakeStartBlockHeight: bkt.UnstakedAt, | ||
AutoStake: bkt.UnlockedAt == maxBlockNumber, | ||
Candidate: bkt.Candidate, | ||
Owner: bkt.Owner, | ||
ContractAddress: contractAddr, | ||
} | ||
if bkt.UnlockedAt != maxBlockNumber { | ||
vb.StakeStartBlockHeight = bkt.UnlockedAt | ||
} | ||
return &vb | ||
} | ||
|
||
func batchAssembleVoteBucket(idxs []uint64, bkts []*Bucket, contractAddr string, blockInterval time.Duration) []*VoteBucket { | ||
vbs := make([]*VoteBucket, 0, len(idxs)) | ||
for i := range idxs { | ||
if bkts[i] == nil { | ||
vbs = append(vbs, nil) | ||
continue | ||
} | ||
vbs = append(vbs, assembleVoteBucket(idxs[i], bkts[i], contractAddr, blockInterval)) | ||
} | ||
return vbs | ||
} |
Oops, something went wrong.