Skip to content

Commit

Permalink
feat: added account indexer
Browse files Browse the repository at this point in the history
  • Loading branch information
javiersuweijie committed Jul 21, 2023
1 parent 9e7e4f1 commit eff1a9d
Show file tree
Hide file tree
Showing 11 changed files with 643 additions and 34 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ else
go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/feather-core ./cmd/feather-core
endif

build-mantlemint: go.sum
ifeq ($(OS),Windows_NT)
exit 1
else
go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/mantlemint ./mantlemint/sync.go
endif

build-contract-tests-hooks:
ifeq ($(OS),Windows_NT)
go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/contract_tests.exe ./cmd/contract_tests
Expand Down
33 changes: 0 additions & 33 deletions mantlemint/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/crisis"
"github.com/spf13/pflag"
"github.com/spf13/viper"
Expand All @@ -24,8 +22,6 @@ type Config struct {
IndexerDB string
DisableSync bool
EnableExportModule bool
RichlistLength int
RichlistThreshold *sdk.Coin
}

var singleton Config
Expand Down Expand Up @@ -86,35 +82,6 @@ func newConfig() Config {
enableExport := getValidEnv("ENABLE_EXPORT_MODULE")
return enableExport == "true"
}(),

// RichlistLength have to be greater than or equal to 0, richlist function will be off if length is 0
RichlistLength: func() int {
lengthStr := getValidEnv("RICHLIST_LENGTH")
length, err := strconv.Atoi(lengthStr)
if err != nil {
panic(err)
}
if length < 0 {
panic(fmt.Errorf("RICHLIST_LENGTH(%s) is invalid", lengthStr))
}
return length
}(),

// RichlistThreshold (format: {amount}{denom} like 1000000000000uluna)
RichlistThreshold: func() *sdk.Coin {
// don't need to read threshold env if the length of richlist is 0
lengthStr := getValidEnv("RICHLIST_LENGTH")
length, _ := strconv.Atoi(lengthStr)
if length == 0 {
return nil
}

thresholdCoin, err := sdk.ParseCoinNormalized(getValidEnv("RICHLIST_THRESHOLD"))
if err != nil {
panic(fmt.Errorf("RICHLIST_THRESHOLD is invalid: %v", err))
}
return &thresholdCoin
}(),
}

viper.SetConfigType("toml")
Expand Down
210 changes: 210 additions & 0 deletions mantlemint/indexer/accounttx/accounttx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package accounttx

import (
"encoding/json"
"fmt"
tmdb "github.com/cometbft/cometbft-db"
abci "github.com/cometbft/cometbft/abci/types"
tmjson "github.com/cometbft/cometbft/libs/json"
tm "github.com/cometbft/cometbft/types"
sdk "github.com/cosmos/cosmos-sdk/types"
terra "github.com/terra-money/alliance/app"
"github.com/terra-money/feather-core/app"

"github.com/terra-money/feather-core/mantlemint/db/safe_batch"
"github.com/terra-money/feather-core/mantlemint/indexer"
"github.com/terra-money/feather-core/mantlemint/indexer/tx"
"github.com/terra-money/feather-core/mantlemint/mantlemint"
)

var cdc = app.MakeEncodingConfig()

var IndexTx = indexer.CreateIndexer(func(db safe_batch.SafeBatchDB, block *tm.Block, blockID *tm.BlockID, evc *mantlemint.EventCollector, _ *app.App) error {
for i, txByte := range block.Txs {
txRecord := evc.ResponseDeliverTxs[i]
addrsInTx, err := parseTxBytesForAddresses(txByte)
if err != nil {
return err
}
addrsInEvents, err := parseEventsForAddresses(txRecord.Events)
if err != nil {
return err
}

for k, _ := range addrsInEvents {
addrsInTx[k] = true
}

for addr, _ := range addrsInTx {
key := GetAccountTxKey(addr, uint64(block.Height), uint64(i))
accountTx := AccountTx{
TxHash: fmt.Sprintf("%X", txByte.Hash()),
BlockHeight: uint64(block.Height),
Timestamp: block.Time,
}
b, err := json.Marshal(accountTx)
if err != nil {
return err
}
err = db.Set(key, b)
if err != nil {
return err
}
}
}
return nil
})

func parseTxBytesForAddresses(txByte []byte) (addrs map[string]bool, err error) {
// Use a map to collect unique addresses
addrs = make(map[string]bool)

// Decode to Tx struct
tx, err := cdc.TxConfig.TxDecoder()(txByte)
if err != nil {
return addrs, err
}

wrappedTx, err := cdc.TxConfig.WrapTxBuilder(tx)
if err != nil {
return addrs, err
}

signers := wrappedTx.GetTx().GetSigners()
for _, signer := range signers {
addr, err := sdk.Bech32ifyAddressBytes(terra.AccountAddressPrefix, signer)
if err != nil {
return addrs, err
}
addrs[addr] = true
}

// Encode to JSON
jsonByte, err := cdc.TxConfig.TxJSONEncoder()(tx)
if err != nil {
return addrs, err
}
// Decode to generic interface to find addresses
var txRaw map[string]interface{}
err = json.Unmarshal(jsonByte, &txRaw)
if err != nil {
return addrs, err
}
bodyRaw, found := txRaw["body"]
if !found {
return addrs, nil
}

body, ok := bodyRaw.(map[string]interface{})
if !ok {
return addrs, fmt.Errorf("unable to coerce tx body into map")
}

var findAddresses func(o interface{})
findAddresses = func(o interface{}) {
stringValue, isString := o.(string)
if isString {
_, err := sdk.GetFromBech32(stringValue, terra.AccountAddressPrefix)
if err == nil {
addrs[stringValue] = true
}
return
}

mapValue, isMap := o.(map[string]interface{})
if isMap {
for _, v := range mapValue {
findAddresses(v)
}
return
}

arrayValue, isArray := o.([]interface{})
if isArray {
for _, a := range arrayValue {
findAddresses(a)
}
}
}

msgsRaw, found := body["messages"]
if !found {
return addrs, nil
}
findAddresses(msgsRaw)
return addrs, nil
}

func parseEventsForAddresses(events []abci.Event) (addrs map[string]bool, err error) {
addrs = make(map[string]bool)
for _, event := range events {
attrs := event.GetAttributes()
for _, attr := range attrs {
addrStr := string(attr.GetValue())
_, err := sdk.GetFromBech32(addrStr, terra.AccountAddressPrefix)
if err == nil {
addrs[addrStr] = true
}
}
}
return addrs, nil
}

func getTxnsByAccount(db tmdb.DB, account string, offset uint64, limit uint64) (txs []tx.TxByHeightRecord, err error) {
key := GetAccountTxKeyByAddr(account)
iter, err := db.Iterator(key, sdk.PrefixEndBytes(key))
if err != nil {
return txs, err
}
currentOffset := uint64(0)
currentLimit := uint64(0)
for iter.Valid() {
if currentOffset < offset {
currentOffset += 1
continue
}
var accountTx AccountTx
b := iter.Value()
err = json.Unmarshal(b, &accountTx)
if err != nil {
return txs, err
}
txRecord, err := tx.GetTxByHash(db, accountTx.TxHash)
if err != nil {
return txs, err
}

var txResponse tx.ResponseDeliverTx
err = tmjson.Unmarshal(txRecord.TxResponse, &txResponse)
if err != nil {
return txs, err
}
txRes := tx.TxByHeightRecord{
Code: txResponse.Code,
Codespace: txResponse.Codespace,
GasUsed: txResponse.GasUsed,
GasWanted: txResponse.GasWanted,
Height: int64(accountTx.BlockHeight),
RawLog: txResponse.Log,
Logs: func() json.RawMessage {
if txResponse.Code == 0 {
return []byte(txResponse.Log)
} else {
out, _ := json.Marshal([]string{})
return out
}
}(),
TxHash: accountTx.TxHash,
Timestamp: accountTx.Timestamp,
Tx: txRecord.Tx,
}

txs = append(txs, txRes)
currentLimit += 1
if currentLimit >= limit {
break
}
iter.Next()
}
return txs, nil
}
51 changes: 51 additions & 0 deletions mantlemint/indexer/accounttx/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package accounttx

import (
tmdb "github.com/cometbft/cometbft-db"
tmjson "github.com/cometbft/cometbft/libs/json"
"github.com/gorilla/mux"
"github.com/terra-money/feather-core/mantlemint/indexer"
"net/http"
"strconv"
)

var (
defaultLimit = uint64(100)
defaultOffset = uint64(0)
EndpointGETAccountTxs = "/index/account/{account}"
)

var RegisterRESTRoute = indexer.CreateRESTRoute(func(router *mux.Router, indexerDB tmdb.DB) {
router.HandleFunc(EndpointGETAccountTxs, func(w http.ResponseWriter, r *http.Request) {
account, ok := mux.Vars(r)["account"]
if !ok {
http.Error(w, "invalid request: account not found", 400)
return
}
queries := r.URL.Query()
offset, err := strconv.ParseUint(queries.Get("offset"), 10, 64)
if err != nil {
offset = defaultOffset
}
limit, err := strconv.ParseUint(queries.Get("offset"), 10, 64)
if err != nil {
limit = defaultLimit
}
txs, err := getTxnsByAccount(indexerDB, account, offset, limit)
if err != nil {
http.Error(w, err.Error(), 500)
}

rxRes := &GetAccountTxsResponse{
Limit: limit,
Offset: offset,
Txs: txs,
}
res, err := tmjson.Marshal(rxRes)
if err != nil {
http.Error(w, err.Error(), 500)
}
w.WriteHeader(200)
w.Write(res)
})
})
34 changes: 34 additions & 0 deletions mantlemint/indexer/accounttx/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package accounttx

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/terra-money/feather-core/mantlemint/indexer/tx"
"time"
)

type AccountTx struct {
TxHash string `json:"txhash"`
BlockHeight uint64 `json:"height"`
Timestamp time.Time `json:"timestamp"`
}

var (
AccountTxPrefix = []byte("account_tx/address:")
)

func GetAccountTxKeyByAddr(addr string) (key []byte) {
key = append(AccountTxPrefix, addr...)
return key
}

func GetAccountTxKey(addr string, blockHeight uint64, txIndex uint64) (key []byte) {
key = append(GetAccountTxKeyByAddr(addr), sdk.Uint64ToBigEndian(blockHeight)...)
key = append(key, sdk.Uint64ToBigEndian(txIndex)...)
return key
}

type GetAccountTxsResponse struct {
Limit uint64 `json:"limit"`
Offset uint64 `json:"offset"`
Txs []tx.TxByHeightRecord `json:"txs"`
}
21 changes: 21 additions & 0 deletions mantlemint/indexer/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,24 @@ var IndexBlock = indexer.CreateIndexer(func(indexerDB safe_batch.SafeBatchDB, bl

return indexerDB.Set(getKey(uint64(block.Height)), recordJSON)
})

func IterateBlocks(indexerDb safe_batch.SafeBatchDB, start int64, end int64, cb func(block *tm.Block) (stop bool)) (err error) {
iter, err := indexerDb.Iterator(getKey(uint64(start)), getKey(uint64(end)))
if err != nil {
return err
}
for iter.Valid() {
b := iter.Value()
var block BlockRecord
err := tmjson.Unmarshal(b, &block)
if err != nil {
return err
}
stop := cb(block.Block)
if stop {
return nil
}
iter.Next()
}
return nil
}
Loading

0 comments on commit eff1a9d

Please sign in to comment.