├── .dockerignore ├── .tool-versions ├── common ├── geolite2 │ ├── mmdb │ │ └── .geoipupdate.lock │ ├── client_test.go │ └── client.go ├── ethereum │ └── constant.go ├── httputil │ ├── http_test.go │ └── http.go ├── signer │ ├── client.go │ └── transaction_args.go ├── txmgr │ └── config.go └── crypto │ └── signature.go ├── .gitattributes ├── internal ├── service │ ├── server.go │ ├── hub │ │ ├── model │ │ │ ├── nta │ │ │ │ ├── response.go │ │ │ │ ├── node_hide_tax_rate.go │ │ │ │ ├── node_challenge.go │ │ │ │ ├── transaction.go │ │ │ │ ├── network.go │ │ │ │ ├── node_operation_profit.go │ │ │ │ ├── node_register.go │ │ │ │ ├── node_info.go │ │ │ │ ├── snapshot.go │ │ │ │ ├── node_event.go │ │ │ │ ├── stake_chip.go │ │ │ │ └── stake_staking.go │ │ │ ├── errorx │ │ │ │ └── error.go │ │ │ └── dsl │ │ │ │ └── decentralized.go │ │ ├── fx.go │ │ └── handler │ │ │ ├── dsl │ │ │ ├── ai.go │ │ │ ├── rss.go │ │ │ ├── dsl.go │ │ │ ├── enforcer │ │ │ │ └── node_request.go │ │ │ └── metrics.go │ │ │ └── nta │ │ │ ├── metrics.go │ │ │ ├── node_challenge.go │ │ │ ├── node_operation.go │ │ │ ├── node_event.go │ │ │ ├── node_hide_tax_rate.go │ │ │ ├── snapshot_staker.go │ │ │ ├── nta.go │ │ │ └── network.go │ ├── indexer │ │ ├── fx.go │ │ └── internal │ │ │ └── handler │ │ │ └── l2 │ │ │ └── handler_chips.go │ ├── settler │ │ ├── fx.go │ │ ├── utils.go │ │ └── utils_test.go │ ├── scheduler │ │ ├── fx.go │ │ ├── enforcer │ │ │ ├── node_status │ │ │ │ └── node_status.go │ │ │ ├── reliability_score │ │ │ │ └── reliability_score.go │ │ │ └── federated_handles │ │ │ │ └── federated_handles.go │ │ ├── server.go │ │ ├── snapshot │ │ │ ├── node_count │ │ │ │ └── node_count.go │ │ │ ├── staker_count │ │ │ │ └── staker_count.go │ │ │ └── server.go │ │ ├── detector │ │ │ └── detector.go │ │ └── taxer │ │ │ └── taxer.go │ └── fx.go ├── database │ ├── driver.go │ └── dialer │ │ ├── postgres │ │ ├── migration │ │ │ └── 20250311171451_add_is_ai_node_field_node_stat.sql │ │ ├── table │ │ │ ├── node_snapshot.go │ │ │ ├── staker_count_snapshot.go │ │ │ ├── checkpoint.go │ │ │ ├── stake_stacking.go │ │ │ ├── node_worker.go │ │ │ ├── node_apy_snapshot.go │ │ │ ├── epoch_trigger.go │ │ │ ├── epoch_apy_snapshot.go │ │ │ ├── stake_chip.go │ │ │ ├── average_tax_rate_submission.go │ │ │ ├── bridge_event.go │ │ │ ├── operator_profit_snapshot.go │ │ │ ├── stake_event.go │ │ │ ├── staker_profit_snapshot.go │ │ │ ├── stake_transaction.go │ │ │ ├── node_reward_record.go │ │ │ ├── node_invalid_response.go │ │ │ ├── node_event.go │ │ │ ├── bridge_transaction.go │ │ │ ├── node_stat.go │ │ │ └── epoch.go │ │ ├── client_checkpoint.go │ │ ├── client_average_tax_submission.go │ │ └── client.go │ │ └── dialer.go ├── constant │ ├── service.go │ └── version.go ├── provider │ ├── http.go │ ├── geolite2.go │ ├── config.go │ ├── nameresolver.go │ ├── redis.go │ ├── ethereum.go │ ├── database.go │ ├── opentelemetry.go │ └── txmgr.go ├── config │ └── flag │ │ └── flag.go ├── nameresolver │ ├── name_service.go │ └── resolver_test.go ├── client │ └── ethereum │ │ └── client.go ├── cronjob │ └── cronjob.go └── cache │ └── client.go ├── deploy ├── README.md └── config.example.yaml ├── contract ├── lens │ └── contract.go ├── crossbell │ └── contract.go ├── multicall3 │ └── contract.go └── l2 │ ├── multicall_test.go │ └── multicall.go ├── README.md ├── .pre-commit-config.yaml ├── .github ├── dependabot.yml └── workflows │ ├── lint.yaml │ └── test.yaml ├── schema ├── node_snapshot.go ├── epoch_apy_snapshot.go ├── staker_count_snapshot.go ├── node_indexer.go ├── checkpoint.go ├── stake_staker.go ├── node_apy_snapshot.go ├── average_tax_rate_submission.go ├── epoch_trigger.go ├── stake_staking.go ├── operator_profit_snapshot.go ├── staker_profit_snapshot.go ├── stake_chip.go ├── node_invalid_response.go ├── stake_event.go ├── stake_transaction.go ├── node_stat.go ├── bridge_transaction.go ├── epoch.go ├── bridge_event.go └── node_event.go ├── .gitignore ├── Makefile ├── .run └── run hub with testnet.run.xml ├── .golangci.yaml ├── Dockerfile ├── docker-compose.yaml ├── LICENSE ├── docs └── openapi_test.go └── cmd └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.21.8 2 | -------------------------------------------------------------------------------- /common/geolite2/mmdb/.geoipupdate.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mmdb filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /internal/service/server.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "context" 4 | 5 | type Server interface { 6 | Name() string 7 | Run(ctx context.Context) error 8 | } 9 | -------------------------------------------------------------------------------- /internal/database/driver.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type Driver string 4 | 5 | const ( 6 | DriverPostgres Driver = "postgres" 7 | DriverMySQL Driver = "mysql" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/response.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | type Response struct { 4 | Data any `json:"data"` 5 | Cursor string `json:"cursor,omitempty"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/constant/service.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import "fmt" 4 | 5 | var ServiceName string 6 | 7 | func BuildServiceName() string { 8 | return fmt.Sprintf("global-indexer.%s", ServiceName) 9 | } 10 | -------------------------------------------------------------------------------- /internal/provider/http.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/common/httputil" 5 | ) 6 | 7 | func ProvideHTTPClient() (httputil.Client, error) { 8 | return httputil.NewHTTPClient() 9 | } 10 | -------------------------------------------------------------------------------- /common/ethereum/constant.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | var ( 6 | AddressGenesis = common.HexToAddress("0x0000000000000000000000000000000000000000") 7 | HashGenesis = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_hide_tax_rate.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type NodeHideTaxRateRequest struct { 6 | NodeAddress common.Address `param:"node_address" validate:"required"` 7 | Signature string `json:"signature" validate:"required"` 8 | } 9 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | ## config.yaml Setup 2 | 3 | Before running the Global Indexer, you need to set up the `config.yaml` file. 4 | 5 | 1. Copy `config.example.yaml` to `config.yaml`: 6 | ```bash 7 | cp config.example.yaml config.yaml 8 | ``` 9 | 1. Edit `config.yaml` and fill in all the environment variables. 10 | -------------------------------------------------------------------------------- /internal/provider/geolite2.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/common/geolite2" 5 | "github.com/rss3-network/global-indexer/internal/config" 6 | ) 7 | 8 | func ProvideGeoIP2(configFile *config.File) *geolite2.Client { 9 | return geolite2.NewClient(configFile.GeoIP) 10 | } 11 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_challenge.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type NodeChallengeRequest struct { 6 | NodeAddress common.Address `param:"node_address" validate:"required"` 7 | Type string `query:"type"` 8 | } 9 | 10 | type NodeChallengeResponseData string 11 | -------------------------------------------------------------------------------- /internal/provider/config.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/internal/config" 5 | "github.com/rss3-network/global-indexer/internal/config/flag" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func ProvideConfig() (*config.File, error) { 10 | return config.Setup(viper.GetString(flag.KeyConfig)) 11 | } 12 | -------------------------------------------------------------------------------- /internal/service/indexer/fx.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/internal/provider" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Options( 9 | fx.Provide(provider.ProvideDatabaseClient), 10 | fx.Provide(provider.ProvideRedisClient), 11 | fx.Provide(provider.ProvideEthereumMultiChainClient), 12 | ) 13 | -------------------------------------------------------------------------------- /contract/lens/contract.go: -------------------------------------------------------------------------------- 1 | package lens 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | //go:generate go run --mod=mod github.com/ethereum/go-ethereum/cmd/abigen@v1.13.5 --abi ./abi/LensHandle.abi --pkg lens --type LensHandle --out contract_lens_handle.go 6 | 7 | var ( 8 | AddressLensHandle = common.HexToAddress("0xe7E7EaD361f3AaCD73A61A9bD6C10cA17F38E945") 9 | ) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSS3 Global Indexer 2 | 3 | The RSS3 Global Indexer, an RSS3 Data Sublayer (DSL) component, is responsible for facilitating coordination among RSS3 Nodes, engaging with the VSL and performing critical functions such as Network Rewards calculation and slashing enforcement. 4 | 5 | ## License 6 | 7 | 8 | 9 | [MIT](LICENSE). 10 | -------------------------------------------------------------------------------- /contract/crossbell/contract.go: -------------------------------------------------------------------------------- 1 | package crossbell 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | //go:generate go run --mod=mod github.com/ethereum/go-ethereum/cmd/abigen@v1.13.5 --abi ./abi/Character.abi --pkg crossbell --type Character --out contract_character.go 6 | 7 | var ( 8 | AddressCharacter = common.HexToAddress("0xa6f969045641Cf486a747A2688F3a5A6d43cd0D8") 9 | ) 10 | -------------------------------------------------------------------------------- /internal/provider/nameresolver.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rss3-network/global-indexer/internal/config" 7 | "github.com/rss3-network/global-indexer/internal/nameresolver" 8 | ) 9 | 10 | func ProvideNameResolver(configFile *config.File) (*nameresolver.NameResolver, error) { 11 | return nameresolver.NewNameResolver(context.TODO(), configFile.RPC.RPCNetwork) 12 | } 13 | -------------------------------------------------------------------------------- /internal/constant/version.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ServiceVersion string 7 | ServiceCommit string 8 | ) 9 | 10 | func BuildServiceVersion() string { 11 | if ServiceVersion == "" { 12 | ServiceVersion = "0.0.0" 13 | } 14 | 15 | if ServiceCommit == "" { 16 | ServiceCommit = "000000" 17 | } 18 | 19 | return fmt.Sprintf("%s (%s)", ServiceVersion, ServiceCommit) 20 | } 21 | -------------------------------------------------------------------------------- /internal/config/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/contract/l1" 5 | "github.com/rss3-network/global-indexer/contract/l2" 6 | ) 7 | 8 | const ( 9 | KeyConfig = "config" 10 | KeyServer = "server" 11 | 12 | KeyChainIDL1 = "chain-id.l1" 13 | KeyChainIDL2 = "chain-id.l2" 14 | ) 15 | 16 | const ( 17 | ValueChainIDL1 = l1.ChainIDMainnet 18 | ValueChainIDL2 = l2.ChainIDMainnet 19 | ) 20 | -------------------------------------------------------------------------------- /internal/service/settler/fx.go: -------------------------------------------------------------------------------- 1 | package settler 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/internal/provider" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Options( 9 | fx.Provide(provider.ProvideDatabaseClient), 10 | fx.Provide(provider.ProvideRedisClient), 11 | fx.Provide(provider.ProvideEthereumMultiChainClient), 12 | fx.Provide(provider.ProvideTxManager), 13 | fx.Provide(provider.ProvideHTTPClient), 14 | ) 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/golangci/golangci-lint 8 | rev: v1.55.2 9 | hooks: 10 | - id: golangci-lint 11 | - repo: https://github.com/commitizen-tools/commitizen 12 | rev: v2.42.1 13 | hooks: 14 | - id: commitizen 15 | -------------------------------------------------------------------------------- /internal/service/scheduler/fx.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/internal/provider" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Options( 9 | fx.Provide(provider.ProvideDatabaseClient), 10 | fx.Provide(provider.ProvideRedisClient), 11 | fx.Provide(provider.ProvideEthereumMultiChainClient), 12 | fx.Provide(provider.ProvideHTTPClient), 13 | fx.Provide(provider.ProvideTxManager), 14 | ) 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | target-branch: main 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | target-branch: main 13 | - package-ecosystem: docker 14 | directory: / 15 | schedule: 16 | interval: daily 17 | target-branch: main 18 | 19 | -------------------------------------------------------------------------------- /internal/provider/redis.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redis/go-redis/v9" 7 | "github.com/rss3-network/global-indexer/internal/config" 8 | ) 9 | 10 | func ProvideRedisClient(config *config.File) (*redis.Client, error) { 11 | options, err := redis.ParseURL(config.Redis.URI) 12 | if err != nil { 13 | return nil, fmt.Errorf("parse redis uri: %w", err) 14 | } 15 | 16 | return redis.NewClient(options), nil 17 | } 18 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/transaction.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type TransactionEventBlock struct { 10 | Hash common.Hash `json:"hash"` 11 | Number *big.Int `json:"number"` 12 | Timestamp int64 `json:"timestamp"` 13 | } 14 | 15 | type TransactionEventTransaction struct { 16 | Hash common.Hash `json:"hash"` 17 | Index uint `json:"index"` 18 | } 19 | -------------------------------------------------------------------------------- /schema/node_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | type NodeSnapshotImporter interface { 6 | Import(nodeSnapshot NodeSnapshot) error 7 | } 8 | 9 | type NodeSnapshotExporter interface { 10 | Export() (*NodeSnapshot, error) 11 | } 12 | 13 | type NodeSnapshotTransformer interface { 14 | NodeSnapshotImporter 15 | NodeSnapshotExporter 16 | } 17 | 18 | type NodeSnapshot struct { 19 | Date time.Time `json:"date"` 20 | Count int64 `json:"count"` 21 | } 22 | -------------------------------------------------------------------------------- /schema/epoch_apy_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/shopspring/decimal" 7 | ) 8 | 9 | type EpochAPYSnapshot struct { 10 | Date time.Time `json:"date"` 11 | EpochID uint64 `json:"epoch_id"` 12 | APY decimal.Decimal `json:"apy"` 13 | CreatedAt time.Time `json:"-"` 14 | UpdatedAt time.Time `json:"-"` 15 | } 16 | 17 | type EpochAPYSnapshotQuery struct { 18 | EpochID *uint64 19 | Limit *int 20 | } 21 | -------------------------------------------------------------------------------- /internal/provider/ethereum.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rss3-network/global-indexer/internal/client/ethereum" 7 | "github.com/rss3-network/global-indexer/internal/config" 8 | ) 9 | 10 | func ProvideEthereumMultiChainClient(configFile *config.File) (*ethereum.MultiChainClient, error) { 11 | endpoints := []string{ 12 | configFile.RSS3Chain.EndpointL1, 13 | configFile.RSS3Chain.EndpointL2, 14 | } 15 | 16 | return ethereum.Dial(context.TODO(), endpoints) 17 | } 18 | -------------------------------------------------------------------------------- /internal/nameresolver/name_service.go: -------------------------------------------------------------------------------- 1 | package nameresolver 2 | 3 | //go:generate go run --mod=mod github.com/dmarkham/enumer --values --type=NameService --linecomment --output name_service_string.go --json --sql 4 | type NameService int 5 | 6 | const ( 7 | NameServiceUnknown NameService = iota // unknown 8 | NameServiceENS // eth 9 | NameServiceCSB // csb 10 | NameServiceLens // lens 11 | NameServiceFarcaster // fc 12 | ) 13 | -------------------------------------------------------------------------------- /internal/service/hub/fx.go: -------------------------------------------------------------------------------- 1 | package hub 2 | 3 | import ( 4 | "github.com/rss3-network/global-indexer/internal/provider" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | var Module = fx.Options( 9 | fx.Provide(provider.ProvideDatabaseClient), 10 | fx.Provide(provider.ProvideRedisClient), 11 | fx.Provide(provider.ProvideEthereumMultiChainClient), 12 | fx.Provide(provider.ProvideGeoIP2), 13 | fx.Provide(provider.ProvideNameResolver), 14 | fx.Provide(provider.ProvideHTTPClient), 15 | fx.Provide(provider.ProvideTxManager), 16 | ) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Visual Studio Code 2 | .vscode/* 3 | 4 | 5 | ### JetBrains 6 | .idea/* 7 | 8 | !.idea/icon.svg 9 | 10 | ### Linux 11 | 12 | ### Go template 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | ### macOS 29 | .DS_Store 30 | 31 | ### Project 32 | /build/* 33 | 34 | ### Configuration 35 | deploy/config.yaml -------------------------------------------------------------------------------- /schema/staker_count_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "time" 4 | 5 | type StakerCountSnapshotImporter interface { 6 | Import(stakeSnapshot StakerCountSnapshot) error 7 | } 8 | 9 | type StakerCountSnapshotExporter interface { 10 | Export() (*StakerCountSnapshot, error) 11 | } 12 | 13 | type StakeSnapshotTransformer interface { 14 | StakerCountSnapshotImporter 15 | StakerCountSnapshotExporter 16 | } 17 | 18 | type StakerCountSnapshot struct { 19 | Date time.Time `json:"date"` 20 | Count int64 `json:"count"` 21 | } 22 | -------------------------------------------------------------------------------- /schema/node_indexer.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type Worker struct { 6 | EpochID uint64 `json:"epoch_id"` 7 | Address common.Address `json:"address"` 8 | Network string `json:"network"` 9 | Name string `json:"name"` 10 | IsActive bool `json:"is_active"` 11 | } 12 | 13 | type WorkerQuery struct { 14 | NodeAddresses []common.Address 15 | Networks []string 16 | Names []string 17 | EpochID uint64 18 | IsActive *bool 19 | } 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --tags --abbrev=0) 2 | 3 | ifeq ($(VERSION),) 4 | VERSION="0.0.0" 5 | endif 6 | 7 | lint: 8 | go mod tidy 9 | go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.58.1 run 10 | 11 | test: 12 | go test -cover -race -v ./... 13 | 14 | .PHONY: build 15 | build: 16 | mkdir -p ./build 17 | go build \ 18 | -o ./build/rss3-global-indexer ./cmd 19 | 20 | image: 21 | docker build \ 22 | --tag rss3-network/global-indexer:$(VERSION) \ 23 | . 24 | 25 | run: 26 | ENVIRONMENT=development go run ./cmd 27 | -------------------------------------------------------------------------------- /schema/checkpoint.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "github.com/ethereum/go-ethereum/common" 4 | 5 | type CheckpointImporter interface { 6 | Import(checkpoint Checkpoint) error 7 | } 8 | 9 | type CheckpointExporter interface { 10 | Export() (*Checkpoint, error) 11 | } 12 | 13 | type CheckpointTransformer interface { 14 | CheckpointImporter 15 | CheckpointExporter 16 | } 17 | 18 | type Checkpoint struct { 19 | ChainID uint64 `json:"network"` 20 | BlockNumber uint64 `json:"block_number"` 21 | BlockHash common.Hash `json:"block_hash"` 22 | } 23 | -------------------------------------------------------------------------------- /schema/stake_staker.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/shopspring/decimal" 6 | ) 7 | 8 | type StakeStaker struct { 9 | Address common.Address `json:"address"` 10 | TotalStakedNodes uint64 `json:"total_staked_nodes"` 11 | TotalChips uint64 `json:"total_chips"` 12 | TotalStakedTokens decimal.Decimal `json:"total_staked_tokens"` 13 | CurrentStakedTokens decimal.Decimal `json:"current_staked_tokens"` // Exclude the staked tokens that are already withdrawn. 14 | } 15 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/migration/20250311171451_add_is_ai_node_field_node_stat.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in this section is executed when the migration is applied. 3 | ALTER TABLE node_stat 4 | ADD COLUMN is_ai_node BOOLEAN NOT NULL DEFAULT FALSE; 5 | 6 | CREATE INDEX IF NOT EXISTS idx_node_stat_is_ai_node_points 7 | ON node_stat (is_ai_node, points DESC); 8 | 9 | -- +goose Down 10 | -- SQL in this section is executed when the migration is rolled back. 11 | DROP INDEX IF EXISTS idx_node_stat_is_ai_node_points; 12 | 13 | ALTER TABLE node_stat 14 | DROP COLUMN is_ai_node; -------------------------------------------------------------------------------- /internal/database/dialer/dialer.go: -------------------------------------------------------------------------------- 1 | package dialer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rss3-network/global-indexer/internal/config" 8 | "github.com/rss3-network/global-indexer/internal/database" 9 | "github.com/rss3-network/global-indexer/internal/database/dialer/postgres" 10 | ) 11 | 12 | func Dial(ctx context.Context, config *config.Database) (database.Client, error) { 13 | switch config.Driver { 14 | case database.DriverPostgres: 15 | return postgres.Dial(ctx, config.URI) 16 | default: 17 | return nil, fmt.Errorf("unsupported driver: %s", config.Driver) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - "deploy/**" 8 | pull_request: 9 | paths-ignore: 10 | - "deploy/**" 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: "1.21" 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: GolangCI Lint 24 | uses: golangci/golangci-lint-action@v6 25 | with: 26 | version: v1.55.2 27 | -------------------------------------------------------------------------------- /.run/run hub with testnet.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /schema/node_apy_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type NodeAPYSnapshot struct { 11 | ID uint64 `json:"id"` 12 | Date time.Time `json:"date"` 13 | EpochID uint64 `json:"epoch_id"` 14 | NodeAddress common.Address `json:"node_address"` 15 | APY decimal.Decimal `json:"apy"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | } 19 | 20 | type NodeAPYSnapshotQuery struct { 21 | NodeAddress *common.Address 22 | } 23 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | linters: 5 | enable: 6 | - asasalint 7 | - asciicheck 8 | - bodyclose 9 | - durationcheck 10 | - errcheck 11 | - errorlint 12 | - gci 13 | - gocyclo 14 | - gosec 15 | - govet 16 | - ineffassign 17 | - makezero 18 | - nakedret 19 | - noctx 20 | - paralleltest 21 | - prealloc 22 | - predeclared 23 | - reassign 24 | - revive 25 | - staticcheck 26 | - stylecheck 27 | - unconvert 28 | - unparam 29 | - unused 30 | - whitespace 31 | - wsl 32 | 33 | issues: 34 | max-issues-per-linter: 0 # Unlimited 35 | max-same-issues: 0 # Unlimited 36 | -------------------------------------------------------------------------------- /schema/average_tax_rate_submission.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type AverageTaxRateSubmission struct { 11 | ID uint64 `json:"id"` 12 | EpochID uint64 `json:"epoch_id"` 13 | TransactionHash common.Hash `json:"transaction_hash"` 14 | AverageTaxRate decimal.Decimal `json:"average_tax_rate"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | } 18 | 19 | type AverageTaxRateSubmissionQuery struct { 20 | EpochID *uint64 `json:"epoch_id"` 21 | Limit *int `json:"limit"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/provider/database.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rss3-network/global-indexer/internal/config" 8 | "github.com/rss3-network/global-indexer/internal/database" 9 | "github.com/rss3-network/global-indexer/internal/database/dialer" 10 | ) 11 | 12 | func ProvideDatabaseClient(configFile *config.File) (database.Client, error) { 13 | databaseClient, err := dialer.Dial(context.TODO(), configFile.Database) 14 | if err != nil { 15 | return nil, fmt.Errorf("dial to database: %w", err) 16 | } 17 | 18 | if err := databaseClient.Migrate(context.TODO()); err != nil { 19 | return nil, fmt.Errorf("mrigate database: %w", err) 20 | } 21 | 22 | return databaseClient, nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/network.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | type NetworkRequest struct { 4 | NetworkName string `param:"network_name" validate:"required"` 5 | } 6 | 7 | type WorkerRequest struct { 8 | NetworkRequest 9 | 10 | WorkerName string `param:"worker_name" validate:"required"` 11 | } 12 | 13 | // NetworkParamsData contains the network parameters 14 | type NetworkParamsData struct { 15 | NetworkAssets map[string]Asset `json:"network_assets"` 16 | WorkerAssets map[string]Asset `json:"worker_assets"` 17 | NetworkConfig map[string]any `json:"network_configs"` 18 | } 19 | 20 | type Asset struct { 21 | Type string `json:"type"` 22 | Name string `json:"name"` 23 | Platform string `json:"platform,omitempty"` 24 | IconURL string `json:"icon_url"` 25 | } 26 | -------------------------------------------------------------------------------- /internal/service/settler/utils.go: -------------------------------------------------------------------------------- 1 | package settler 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | "github.com/rss3-network/global-indexer/common/txmgr" 8 | "github.com/rss3-network/global-indexer/contract/l2" 9 | "github.com/rss3-network/global-indexer/schema" 10 | ) 11 | 12 | // prepareInputData encodes input data for the transaction 13 | func (s *Server) prepareInputData(data schema.SettlementData) ([]byte, error) { 14 | input, err := txmgr.EncodeInput(l2.SettlementMetaData.ABI, l2.MethodDistributeRewards, data.Epoch, data.NodeAddress, data.OperationRewards, data.RequestCount, data.IsFinal) 15 | if err != nil { 16 | return nil, fmt.Errorf("encode input: %w", err) 17 | } 18 | 19 | return input, nil 20 | } 21 | 22 | func scaleGwei(in *big.Int) { 23 | in.Mul(in, big.NewInt(1e18)) 24 | } 25 | -------------------------------------------------------------------------------- /schema/epoch_trigger.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | type EpochTrigger struct { 11 | TransactionHash common.Hash `json:"transaction_hash"` 12 | EpochID uint64 `json:"epoch_id"` 13 | Data SettlementData `json:"data"` 14 | CreatedAt time.Time `json:"created_at"` 15 | UpdatedAt time.Time `json:"updated_at"` 16 | } 17 | 18 | type SettlementData struct { 19 | Epoch *big.Int `json:"epoch"` 20 | NodeAddress []common.Address `json:"node_addresses"` 21 | OperationRewards []*big.Int `json:"operation_rewards"` 22 | RequestCount []*big.Int `json:"request_count"` 23 | IsFinal bool `json:"is_final"` 24 | } 25 | -------------------------------------------------------------------------------- /schema/stake_staking.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/shopspring/decimal" 6 | ) 7 | 8 | type StakeStakingExporter interface { 9 | Export() (*StakeStaking, error) 10 | } 11 | 12 | type StakeStakingTransformer interface { 13 | StakeStakingExporter 14 | } 15 | 16 | type StakeStaking struct { 17 | Staker common.Address `json:"staker"` 18 | Node common.Address `json:"node"` 19 | Value decimal.Decimal `json:"value"` 20 | Chips StakeStakingChips `json:"chips"` 21 | } 22 | 23 | type StakeStakingChips struct { 24 | Total uint64 `json:"total"` 25 | Showcase []*StakeChip `json:"showcase"` 26 | } 27 | 28 | type StakeStakingsQuery struct { 29 | Cursor *string 30 | Node *common.Address 31 | Staker *common.Address 32 | Limit int 33 | } 34 | -------------------------------------------------------------------------------- /internal/service/hub/handler/dsl/ai.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func (d *DSL) GetAI(c echo.Context) error { 13 | path := c.Param("*") 14 | query := c.Request().URL.RawQuery 15 | 16 | requestCounter.WithLabelValues("AI").Inc() 17 | 18 | data, err := d.distributor.DistributeAIData(c.Request().Context(), path, query) 19 | 20 | if err != nil { 21 | if errors.Is(err, errorx.ErrNoNodesAvailable) { 22 | return errorx.ServiceUnavailableError(c, err) 23 | } 24 | 25 | zap.L().Error("distribute ai data error", zap.Error(err)) 26 | 27 | return errorx.InternalError(c) 28 | } 29 | 30 | return c.JSONBlob(http.StatusOK, data) 31 | } 32 | -------------------------------------------------------------------------------- /internal/service/hub/handler/dsl/rss.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func (d *DSL) GetRSSHub(c echo.Context) error { 13 | path := c.Param("*") 14 | query := c.Request().URL.RawQuery 15 | 16 | requestCounter.WithLabelValues("GetRSSHub").Inc() 17 | 18 | data, err := d.distributor.DistributeRSSHubData(c.Request().Context(), path, query) 19 | 20 | if err != nil { 21 | if errors.Is(err, errorx.ErrNoNodesAvailable) { 22 | return errorx.ServiceUnavailableError(c, err) 23 | } 24 | 25 | zap.L().Error("distribute rss hub data error", zap.Error(err)) 26 | 27 | return errorx.InternalError(c) 28 | } 29 | 30 | return c.JSONBlob(http.StatusOK, data) 31 | } 32 | -------------------------------------------------------------------------------- /internal/service/settler/utils_test.go: -------------------------------------------------------------------------------- 1 | package settler 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | ) 8 | 9 | func TestScaleGwei(t *testing.T) { 10 | t.Parallel() 11 | 12 | tests := []struct { 13 | name string 14 | in *big.Int 15 | expected *big.Int 16 | }{ 17 | { 18 | name: "1", 19 | in: big.NewInt(1), 20 | expected: big.NewInt(100000000000000000), 21 | }, 22 | { 23 | name: "50", 24 | in: big.NewInt(50), 25 | expected: big.NewInt(5000000000000000000), 26 | }, 27 | } 28 | 29 | for _, tt := range tests { 30 | tt := tt 31 | t.Run(tt.name, func(t *testing.T) { 32 | t.Parallel() 33 | 34 | scaleGwei(tt.in) 35 | 36 | fmt.Println(tt.in, tt.expected) 37 | 38 | if tt.in.Cmp(tt.expected) != 1 { 39 | t.Errorf("got = %v, want %v ", tt.in, tt.expected) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/rss3-network/go-image/go-builder AS base 2 | 3 | WORKDIR /root/gi 4 | 5 | RUN --mount=type=cache,target=/go/pkg/mod/ \ 6 | --mount=type=bind,source=go.sum,target=go.sum \ 7 | --mount=type=bind,source=go.mod,target=go.mod \ 8 | go mod download -x 9 | 10 | COPY . . 11 | 12 | # Download GeoLite2-City.mmdb from Henry's Google Drive 13 | RUN mkdir -p common/geolite2/mmdb && touch common/geolite2/mmdb/.geoipupdate.lock 14 | RUN curl -L "https://drive.google.com/uc?export=download&id=1xyJc_0rupY5MZzdCTOr7sDL--j7r5i0w" -o common/geolite2/mmdb/GeoLite2-City.mmdb 15 | 16 | FROM base AS builder 17 | 18 | ENV CGO_ENABLED=0 19 | RUN --mount=type=cache,target=/go/pkg/mod/ \ 20 | go build cmd/main.go 21 | 22 | FROM ghcr.io/rss3-network/go-image/go-runtime AS runner 23 | 24 | WORKDIR /root/gi 25 | 26 | COPY --from=builder /root/gi/main ./gi 27 | 28 | EXPOSE 80 29 | ENTRYPOINT ["./gi"] 30 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_operation_profit.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type GetNodeOperationProfitRequest struct { 11 | NodeAddress common.Address `param:"node_address" validate:"required"` 12 | } 13 | 14 | type GetNodeOperationProfitResponse struct { 15 | NodeAddress common.Address `json:"node_address"` 16 | OperationPool decimal.Decimal `json:"operation_pool"` 17 | OneDay *NodeProfitChangeDetail `json:"one_day"` 18 | OneWeek *NodeProfitChangeDetail `json:"one_week"` 19 | OneMonth *NodeProfitChangeDetail `json:"one_month"` 20 | } 21 | 22 | type NodeProfitChangeDetail struct { 23 | Date time.Time `json:"date"` 24 | OperationPool decimal.Decimal `json:"operation_pool"` 25 | ProfitAndLoss decimal.Decimal `json:"profit_and_loss"` 26 | } 27 | -------------------------------------------------------------------------------- /schema/operator_profit_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type OperatorProfitSnapshot struct { 11 | Date time.Time `json:"date"` 12 | EpochID uint64 `json:"epoch_id"` 13 | Operator common.Address `json:"operator"` 14 | OperationPool decimal.Decimal `json:"operation_pool"` 15 | ID uint64 `json:"-"` 16 | CreatedAt time.Time `json:"-"` 17 | UpdatedAt time.Time `json:"-"` 18 | } 19 | 20 | type OperatorProfitSnapshotsQuery struct { 21 | Operator *common.Address `json:"operator"` 22 | Limit *int `json:"limit"` 23 | Cursor *string `json:"cursor"` 24 | BeforeDate *time.Time `json:"before_date"` 25 | AfterDate *time.Time `json:"after_date"` 26 | Dates []time.Time `json:"dates"` 27 | } 28 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rss3-network/global-indexer/schema" 7 | gorm "gorm.io/gorm/schema" 8 | ) 9 | 10 | var ( 11 | _ gorm.Tabler = (*NodeSnapshot)(nil) 12 | _ schema.NodeSnapshotTransformer = (*NodeSnapshot)(nil) 13 | ) 14 | 15 | type NodeSnapshot struct { 16 | Date time.Time `gorm:"column:date"` 17 | Count uint64 `gorm:"column:count"` 18 | } 19 | 20 | func (s *NodeSnapshot) TableName() string { 21 | return "node.count_snapshots" 22 | } 23 | 24 | func (s *NodeSnapshot) Import(stakeSnapshot schema.NodeSnapshot) error { 25 | s.Date = stakeSnapshot.Date 26 | s.Count = uint64(stakeSnapshot.Count) 27 | 28 | return nil 29 | } 30 | 31 | func (s *NodeSnapshot) Export() (*schema.NodeSnapshot, error) { 32 | stakeSnapshot := schema.NodeSnapshot{ 33 | Date: s.Date, 34 | Count: int64(s.Count), 35 | } 36 | 37 | return &stakeSnapshot, nil 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - "deploy/**" 8 | pull_request: 9 | paths-ignore: 10 | - "deploy/**" 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: self-hosted 16 | steps: 17 | # - name: Import Secrets 18 | # uses: hashicorp/vault-action@v2.4.0 19 | # with: 20 | # url: ${{ secrets.VAULT_ADDR }} 21 | # token: ${{ secrets.VAULT_TOKEN }} 22 | # secrets: | 23 | # kv/data/network/rss3-node ENDPOINT_ETHEREUM ; 24 | # kv/data/network/rss3-node ENDPOINT_POLYGON ; 25 | # kv/data/network/rss3-node FARCASTER_URI 26 | - name: Setup Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: "1.21" 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | lfs: 'true' 34 | - name: Test 35 | run: make test 36 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | cockroach: 5 | container_name: cockroach 6 | image: cockroachdb/cockroach:v23.1.8 7 | networks: 8 | - default 9 | ports: 10 | - "8080:8080" 11 | - "26257:26257" 12 | command: 13 | - start-single-node 14 | - --cluster-name=rss3-global-indexer 15 | - --insecure 16 | redis: 17 | container_name: redis 18 | image: redis:7.2.4 19 | command: sh -c "redis-server --notify-keyspace-events K$" 20 | networks: 21 | - default 22 | ports: 23 | - "6379:6379" 24 | jaeger: 25 | container_name: jaeger 26 | image: jaegertracing/all-in-one:1.49.0 27 | networks: 28 | - default 29 | ports: 30 | - "4317:4317" # OpenTelemetry Protocol over gRPC 31 | - "4318:4318" # OpenTelemetry Protocol over HTTP 32 | - "5778:5778" # Configurations 33 | - "16686:16686" # Frontend 34 | environment: 35 | COLLECTOR_ZIPKIN_HTTP_PORT: 9411 36 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/staker_count_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rss3-network/global-indexer/schema" 7 | gorm "gorm.io/gorm/schema" 8 | ) 9 | 10 | var ( 11 | _ gorm.Tabler = (*StakerCountSnapshot)(nil) 12 | _ schema.StakeSnapshotTransformer = (*StakerCountSnapshot)(nil) 13 | ) 14 | 15 | type StakerCountSnapshot struct { 16 | Date time.Time `gorm:"column:date"` 17 | Count uint64 `gorm:"column:count"` 18 | } 19 | 20 | func (s *StakerCountSnapshot) TableName() string { 21 | return "stake.count_snapshots" 22 | } 23 | 24 | func (s *StakerCountSnapshot) Import(stakeSnapshot schema.StakerCountSnapshot) error { 25 | s.Date = stakeSnapshot.Date 26 | s.Count = uint64(stakeSnapshot.Count) 27 | 28 | return nil 29 | } 30 | 31 | func (s *StakerCountSnapshot) Export() (*schema.StakerCountSnapshot, error) { 32 | stakeSnapshot := schema.StakerCountSnapshot{ 33 | Date: s.Date, 34 | Count: int64(s.Count), 35 | } 36 | 37 | return &stakeSnapshot, nil 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present, The RSS3 Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_register.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type RegisterNodeRequest struct { 10 | Address common.Address `json:"address" validate:"required"` 11 | Signature string `json:"signature" validate:"required"` 12 | Endpoint string `json:"endpoint" validate:"required"` 13 | Stream json.RawMessage `json:"stream,omitempty"` 14 | Config json.RawMessage `json:"config,omitempty"` 15 | Type string `json:"type" validate:"required,oneof=alpha beta production" default:"alpha"` 16 | AccessToken string `json:"access_token" validate:"required_if=Type production"` 17 | Version string `json:"version" default:"v0.1.0"` 18 | } 19 | 20 | type NodeHeartbeatRequest struct { 21 | Address common.Address `json:"address" validate:"required"` 22 | Signature string `json:"signature" validate:"required"` 23 | Endpoint string `json:"endpoint" validate:"required"` 24 | Timestamp int64 `json:"timestamp" validate:"required"` 25 | } 26 | -------------------------------------------------------------------------------- /schema/staker_profit_snapshot.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type StakerProfitSnapshot struct { 11 | Date time.Time `json:"date"` 12 | EpochID uint64 `json:"epoch_id"` 13 | OwnerAddress common.Address `json:"owner_address"` 14 | TotalChipAmount decimal.Decimal `json:"total_chip_amount"` 15 | TotalChipValue decimal.Decimal `json:"total_chip_value"` 16 | ID uint64 `json:"-"` 17 | CreatedAt time.Time `json:"-"` 18 | UpdatedAt time.Time `json:"-"` 19 | } 20 | 21 | type StakerProfitSnapshotsQuery struct { 22 | Cursor *string `json:"cursor"` 23 | Limit *int `json:"limit"` 24 | OwnerAddress *common.Address `json:"owner_address"` 25 | EpochID *uint64 `json:"epoch_id"` 26 | EpochIDs []uint64 `json:"epoch_ids"` 27 | Dates []time.Time `json:"dates"` 28 | BeforeDate *time.Time `json:"before_date"` 29 | AfterDate *time.Time `json:"after_date"` 30 | } 31 | -------------------------------------------------------------------------------- /common/geolite2/client_test.go: -------------------------------------------------------------------------------- 1 | package geolite2_test 2 | 3 | //import ( 4 | // "context" 5 | // "encoding/json" 6 | // "testing" 7 | // 8 | // "github.com/rss3-network/global-indexer/common/geolite2" 9 | // "github.com/rss3-network/global-indexer/internal/config" 10 | //) 11 | // 12 | //func TestLookupNodeLocation(t *testing.T) { 13 | // t.Parallel() 14 | // 15 | // c := geolite2.NewClient(&config.GeoIP{ 16 | // File: "./mmdb/GeoLite2-City.mmdb", 17 | // }) 18 | // 19 | // testcases := []struct { 20 | // name string 21 | // endpoint string 22 | // }{ 23 | // { 24 | // name: "ip", 25 | // endpoint: "1.2.3.4", 26 | // }, 27 | // { 28 | // name: "domain", 29 | // endpoint: "gi.rss3.dev", 30 | // }, 31 | // } 32 | // 33 | // for _, testcase := range testcases { 34 | // testcase := testcase 35 | // 36 | // t.Run(testcase.name, func(t *testing.T) { 37 | // t.Parallel() 38 | // 39 | // locals, _ := c.LookupNodeLocation(context.Background(), testcase.endpoint) 40 | // //require.NoError(t, err) 41 | // 42 | // data, _ := json.Marshal(locals) 43 | // t.Log(testcase.endpoint, string(data)) 44 | // }) 45 | // } 46 | //} 47 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/client_checkpoint.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rss3-network/global-indexer/internal/database/dialer/postgres/table" 8 | "github.com/rss3-network/global-indexer/schema" 9 | "gorm.io/gorm/clause" 10 | ) 11 | 12 | func (c *client) FindCheckpoint(ctx context.Context, chainID uint64) (*schema.Checkpoint, error) { 13 | var checkpoint table.Checkpoint 14 | 15 | if err := c.database. 16 | WithContext(ctx). 17 | FirstOrInit(&checkpoint, table.Checkpoint{ChainID: chainID}).Error; err != nil { 18 | return nil, err 19 | } 20 | 21 | return checkpoint.Export() 22 | } 23 | 24 | func (c *client) SaveCheckpoint(ctx context.Context, checkpoint *schema.Checkpoint) error { 25 | var value table.Checkpoint 26 | if err := value.Import(*checkpoint); err != nil { 27 | return fmt.Errorf("import checkpoint: %w", err) 28 | } 29 | 30 | clauses := []clause.Expression{ 31 | clause.OnConflict{ 32 | Columns: []clause.Column{{Name: "chain_id"}}, 33 | UpdateAll: true, 34 | }, 35 | } 36 | 37 | return c.database.WithContext(ctx).Clauses(clauses...).Create(&value).Error 38 | } 39 | -------------------------------------------------------------------------------- /internal/service/fx.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rss3-network/global-indexer/internal/constant" 7 | "github.com/rss3-network/global-indexer/internal/provider" 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/trace" 10 | "go.uber.org/fx" 11 | "go.uber.org/fx/fxevent" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func NewServer(options ...fx.Option) *fx.App { 16 | return fx.New( 17 | fx.Options(options...), 18 | fx.Provide(provider.ProvideConfig), 19 | fx.Provide(provider.ProvideOpenTelemetryTracer), 20 | fx.Invoke(InjectLifecycle), 21 | fx.Invoke(InjectOpenTelemetry), 22 | fx.WithLogger(func() fxevent.Logger { 23 | return &fxevent.ZapLogger{ 24 | Logger: zap.L(), 25 | } 26 | }), 27 | ) 28 | } 29 | 30 | func InjectLifecycle(lifecycle fx.Lifecycle, server Server) { 31 | constant.ServiceName = server.Name() 32 | 33 | hook := fx.Hook{ 34 | OnStart: func(ctx context.Context) error { 35 | return server.Run(ctx) 36 | }, 37 | } 38 | 39 | lifecycle.Append(hook) 40 | } 41 | 42 | func InjectOpenTelemetry(tracerProvider trace.TracerProvider) { 43 | otel.SetTracerProvider(tracerProvider) 44 | } 45 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/metrics.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 10 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 11 | "github.com/rss3-network/global-indexer/schema" 12 | ) 13 | 14 | type DslTotalRequestsResponse struct { 15 | TotalRequests int64 `json:"total_requests"` 16 | } 17 | 18 | func (n *NTA) GetDslTotalRequests(c echo.Context) error { 19 | totalRequests, err := n.getNodeTotalRequests(c.Request().Context()) 20 | 21 | if err != nil { 22 | return errorx.InternalError(c) 23 | } 24 | 25 | return c.JSON(http.StatusOK, nta.Response{ 26 | Data: DslTotalRequestsResponse{ 27 | TotalRequests: totalRequests, 28 | }, 29 | }) 30 | } 31 | 32 | func (n *NTA) getNodeTotalRequests(ctx context.Context) (int64, error) { 33 | stats, err := n.databaseClient.FindNodeStats(ctx, &schema.StatQuery{}) 34 | 35 | if err != nil { 36 | return 0, fmt.Errorf("failed to find node stats: %w", err) 37 | } 38 | 39 | var totalRequests int64 40 | 41 | for _, stat := range stats { 42 | totalRequests += stat.TotalRequest 43 | } 44 | 45 | return totalRequests, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_info.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | ) 10 | 11 | type NodeRequest struct { 12 | Address common.Address `param:"node_address" validate:"required"` 13 | } 14 | 15 | type BatchNodeRequest struct { 16 | Cursor *string `query:"cursor"` 17 | Limit int `query:"limit" validate:"min=1,max=100" default:"50"` 18 | NodeAddresses []common.Address `query:"node_addresses"` 19 | } 20 | 21 | type NodeResponseData *schema.Node 22 | 23 | type NodesResponseData []*schema.Node 24 | 25 | func NewNode(node *schema.Node, baseURL url.URL) NodeResponseData { 26 | if node.Avatar != nil { 27 | node.Avatar.Image = baseURL.JoinPath(fmt.Sprintf("/nta/nodes/%s/avatar.svg", node.Address)).String() 28 | } 29 | 30 | if node.HideTaxRate { 31 | node.TaxRateBasisPoints = nil 32 | } 33 | 34 | return node 35 | } 36 | 37 | func NewNodes(nodes []*schema.Node, baseURL url.URL) NodesResponseData { 38 | nodeModels := make([]*schema.Node, 0, len(nodes)) 39 | for _, node := range nodes { 40 | nodeModels = append(nodeModels, NewNode(node, baseURL)) 41 | } 42 | 43 | return nodeModels 44 | } 45 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/checkpoint.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | gorm "gorm.io/gorm/schema" 9 | ) 10 | 11 | var ( 12 | _ gorm.Tabler = (*Checkpoint)(nil) 13 | _ schema.CheckpointTransformer = (*Checkpoint)(nil) 14 | ) 15 | 16 | type Checkpoint struct { 17 | ChainID uint64 `gorm:"column:chain_id"` 18 | BlockNumber uint64 `gorm:"column:block_number"` 19 | BlockHash string `gorm:"column:block_hash"` 20 | CreatedAt time.Time `gorm:"column:created_at"` 21 | UpdatedAt time.Time `gorm:"column:updated_at"` 22 | } 23 | 24 | func (c *Checkpoint) TableName() string { 25 | return "checkpoints" 26 | } 27 | 28 | func (c *Checkpoint) Import(checkpoint schema.Checkpoint) error { 29 | c.ChainID = checkpoint.ChainID 30 | c.BlockNumber = checkpoint.BlockNumber 31 | c.BlockHash = checkpoint.BlockHash.String() 32 | 33 | return nil 34 | } 35 | 36 | func (c *Checkpoint) Export() (*schema.Checkpoint, error) { 37 | checkpoint := schema.Checkpoint{ 38 | ChainID: c.ChainID, 39 | BlockNumber: c.BlockNumber, 40 | BlockHash: common.HexToHash(c.BlockHash), 41 | } 42 | 43 | return &checkpoint, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/stake_stacking.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/rss3-network/global-indexer/schema" 6 | "github.com/shopspring/decimal" 7 | gorm "gorm.io/gorm/schema" 8 | ) 9 | 10 | var ( 11 | _ gorm.Tabler = (*StakeStaking)(nil) 12 | _ schema.StakeStakingTransformer = (*StakeStaking)(nil) 13 | ) 14 | 15 | type StakeStaking struct { 16 | Staker string `gorm:"column:staker"` 17 | Node string `gorm:"column:node"` 18 | Count uint32 `gorm:"column:count"` 19 | Value decimal.Decimal `gorm:"column:value"` 20 | } 21 | 22 | func (s *StakeStaking) TableName() string { 23 | return "stake.stakings" 24 | } 25 | 26 | func (s *StakeStaking) Import(stakeStaking schema.StakeStaking) error { 27 | s.Staker = stakeStaking.Staker.String() 28 | s.Node = stakeStaking.Node.String() 29 | s.Value = stakeStaking.Value 30 | 31 | return nil 32 | } 33 | 34 | func (s *StakeStaking) Export() (*schema.StakeStaking, error) { 35 | stakeStaker := schema.StakeStaking{ 36 | Staker: common.HexToAddress(s.Staker), 37 | Node: common.HexToAddress(s.Node), 38 | Value: s.Value, 39 | Chips: schema.StakeStakingChips{ 40 | Total: uint64(s.Count), 41 | }, 42 | } 43 | 44 | return &stakeStaker, nil 45 | } 46 | -------------------------------------------------------------------------------- /schema/stake_chip.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | type StakeChipImporter interface { 12 | Import(stakeChip StakeChip) error 13 | } 14 | 15 | type StakeChipExporter interface { 16 | Export() (*StakeChip, error) 17 | } 18 | 19 | type StakeChipTransformer interface { 20 | StakeChipImporter 21 | StakeChipExporter 22 | } 23 | 24 | type StakeChip struct { 25 | ID *big.Int `json:"id"` 26 | Owner common.Address `json:"owner"` 27 | Node common.Address `json:"node"` 28 | Value decimal.Decimal `json:"value"` 29 | LatestValue decimal.Decimal `json:"latest_value,omitempty"` 30 | Metadata json.RawMessage `json:"metadata"` 31 | BlockNumber *big.Int `json:"block_number"` 32 | BlockTimestamp uint64 `json:"block_timestamp"` 33 | Finalized bool `json:"finalized"` 34 | } 35 | 36 | type StakeChipQuery struct { 37 | ID *big.Int `query:"id"` 38 | } 39 | 40 | type StakeChipsQuery struct { 41 | Cursor *big.Int 42 | IDs []*big.Int 43 | Node *common.Address 44 | Owner *common.Address 45 | Limit *int 46 | DistinctOwner bool 47 | BlockNumber *big.Int 48 | } 49 | -------------------------------------------------------------------------------- /common/httputil/http_test.go: -------------------------------------------------------------------------------- 1 | package httputil_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/rss3-network/global-indexer/common/httputil" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var ( 14 | setupOnce sync.Once 15 | httpClient httputil.Client 16 | ) 17 | 18 | func setup(t *testing.T) { 19 | setupOnce.Do(func() { 20 | var err error 21 | 22 | httpClient, err = httputil.NewHTTPClient() 23 | require.NoError(t, err) 24 | }) 25 | } 26 | 27 | func TestHTTPClient_FetchWithMethod(t *testing.T) { 28 | t.Parallel() 29 | 30 | setup(t) 31 | 32 | type arguments struct { 33 | url string 34 | } 35 | 36 | testcases := []struct { 37 | name string 38 | arguments arguments 39 | }{ 40 | { 41 | name: "Fetch Arweave", 42 | arguments: arguments{ 43 | url: "https://arweave.net/aMAYipJXf9rVHnwRYnNF7eUCxBc1zfkaopBt5TJwLWw", 44 | }, 45 | }, 46 | { 47 | name: "Fetch External Api", 48 | arguments: arguments{ 49 | url: "https://data.lens.phaver.com/api/lens/posts/1fdcc7ce-91a7-4af7-8022-13132842a5ec", 50 | }, 51 | }, 52 | } 53 | 54 | for _, testcase := range testcases { 55 | testcase := testcase 56 | 57 | t.Run(testcase.name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | _, err := httpClient.FetchWithMethod(context.TODO(), http.MethodGet, testcase.arguments.url, "", nil) 61 | require.NoError(t, err) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/openapi_test.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/protocol-go/schema/metadata" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGenerateMetadataObject(t *testing.T) { 13 | t.Run("embedding of struct", func(t *testing.T) { 14 | t.Parallel() 15 | 16 | result := generateMetadataObject(reflect.TypeOf(metadata.MetaverseTrade{})) 17 | 18 | assert.Equal(t, "object", result["type"]) 19 | assert.Contains(t, result["properties"], "action") 20 | assert.Contains(t, result["properties"], "cost") 21 | assert.Contains(t, result["properties"], "address") 22 | assert.NotContains(t, result["properties"], "Token") 23 | }) 24 | 25 | t.Run("struct array", func(t *testing.T) { 26 | t.Parallel() 27 | 28 | result := generateMetadataObject(reflect.TypeOf(metadata.ExchangeLiquidity{})) 29 | 30 | assert.Equal(t, "object", result["type"]) 31 | assert.Contains(t, result["properties"], "tokens") 32 | }) 33 | 34 | t.Run("common address", func(t *testing.T) { 35 | t.Parallel() 36 | 37 | result := generateMetadataObject(reflect.TypeOf(struct { 38 | Address common.Address `json:"address,omitempty"` 39 | AddressPtr *common.Address `json:"address_ptr,omitempty"` 40 | }{})) 41 | 42 | assert.Equal(t, "object", result["type"]) 43 | assert.Contains(t, result["properties"], "address") 44 | assert.Contains(t, result["properties"], "address_ptr") 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/node_challenge.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 10 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 11 | ) 12 | 13 | var ( 14 | registrationMessage = "I, %s, am signing this message for registering my intention to operate an RSS3 Node." 15 | hideTaxRateMessage = "I, %s, am signing this message for registering my intention to hide the tax rate on Explorer for my RSS3 Node." 16 | ) 17 | 18 | func (n *NTA) GetNodeChallenge(c echo.Context) error { 19 | var request nta.NodeChallengeRequest 20 | 21 | if err := c.Bind(&request); err != nil { 22 | return errorx.BadParamsError(c, fmt.Errorf("bind request: %w", err)) 23 | } 24 | 25 | if err := c.Validate(&request); err != nil { 26 | return errorx.ValidationFailedError(c, fmt.Errorf("validation failed: %w", err)) 27 | } 28 | 29 | var data nta.NodeChallengeResponseData 30 | 31 | switch request.Type { 32 | case "": 33 | data = nta.NodeChallengeResponseData(fmt.Sprintf(registrationMessage, strings.ToLower(request.NodeAddress.String()))) 34 | case "hideTaxRate": 35 | data = nta.NodeChallengeResponseData(fmt.Sprintf(hideTaxRateMessage, strings.ToLower(request.NodeAddress.String()))) 36 | default: 37 | return errorx.BadRequestError(c, fmt.Errorf("invalid challenge type: %s", request.Type)) 38 | } 39 | 40 | return c.JSON(http.StatusOK, nta.Response{ 41 | Data: data, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/service/hub/handler/dsl/dsl.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | 7 | "github.com/rss3-network/global-indexer/common/httputil" 8 | "github.com/rss3-network/global-indexer/common/txmgr" 9 | "github.com/rss3-network/global-indexer/contract/l2" 10 | "github.com/rss3-network/global-indexer/internal/cache" 11 | "github.com/rss3-network/global-indexer/internal/config" 12 | "github.com/rss3-network/global-indexer/internal/database" 13 | "github.com/rss3-network/global-indexer/internal/nameresolver" 14 | "github.com/rss3-network/global-indexer/internal/service/hub/handler/dsl/distributor" 15 | ) 16 | 17 | type DSL struct { 18 | distributor *distributor.Distributor 19 | databaseClient database.Client 20 | cacheClient cache.Client 21 | nameService *nameresolver.NameResolver 22 | } 23 | 24 | func NewDSL(ctx context.Context, databaseClient database.Client, cacheClient cache.Client, nameService *nameresolver.NameResolver, stakingContract *l2.StakingV2MulticallClient, networkParamsContract *l2.NetworkParams, httpClient httputil.Client, txManager *txmgr.SimpleTxManager, settlerConfig *config.Settler, chainID *big.Int) (*DSL, error) { 25 | distributorService, err := distributor.NewDistributor(ctx, databaseClient, cacheClient, httpClient, stakingContract, networkParamsContract, txManager, settlerConfig, chainID) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &DSL{ 31 | distributor: distributorService, 32 | databaseClient: databaseClient, 33 | cacheClient: cacheClient, 34 | nameService: nameService, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_worker.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/rss3-network/global-indexer/schema" 6 | ) 7 | 8 | type Worker struct { 9 | EpochID uint64 `gorm:"column:epoch_id;primaryKey"` 10 | Address common.Address `gorm:"column:address;primaryKey"` 11 | Network string `gorm:"column:network;primaryKey"` 12 | Name string `gorm:"column:name;primaryKey"` 13 | IsActive bool `gorm:"column:is_active"` 14 | } 15 | 16 | func (*Worker) TableName() string { 17 | return "node_worker" 18 | } 19 | 20 | func (w *Worker) Import(worker *schema.Worker) { 21 | w.EpochID = worker.EpochID 22 | w.Address = worker.Address 23 | w.Network = worker.Network 24 | w.Name = worker.Name 25 | w.IsActive = worker.IsActive 26 | } 27 | 28 | func (w *Worker) Export() *schema.Worker { 29 | return &schema.Worker{ 30 | EpochID: w.EpochID, 31 | Address: w.Address, 32 | Network: w.Network, 33 | Name: w.Name, 34 | IsActive: w.IsActive, 35 | } 36 | } 37 | 38 | type Workers []Worker 39 | 40 | func (w *Workers) Export() []*schema.Worker { 41 | workers := make([]*schema.Worker, 0) 42 | 43 | for _, worker := range *w { 44 | exportedWorker := worker.Export() 45 | workers = append(workers, exportedWorker) 46 | } 47 | 48 | return workers 49 | } 50 | 51 | func (w *Workers) Import(workers []*schema.Worker) { 52 | *w = make([]Worker, 0, len(workers)) 53 | 54 | for _, worker := range workers { 55 | var tWorker Worker 56 | 57 | tWorker.Import(worker) 58 | *w = append(*w, tWorker) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/provider/opentelemetry.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rss3-network/global-indexer/internal/config" 8 | "github.com/rss3-network/global-indexer/internal/constant" 9 | "go.opentelemetry.io/otel" 10 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 11 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 12 | "go.opentelemetry.io/otel/sdk/resource" 13 | tracer "go.opentelemetry.io/otel/sdk/trace" 14 | semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 15 | "go.opentelemetry.io/otel/trace" 16 | ) 17 | 18 | func ProvideOpenTelemetryTracer(configFile *config.File) (trace.TracerProvider, error) { 19 | if configFile.Telemetry == nil { 20 | return otel.GetTracerProvider(), nil 21 | } 22 | 23 | options := []otlptracehttp.Option{ 24 | otlptracehttp.WithEndpoint(configFile.Telemetry.Endpoint), 25 | } 26 | 27 | if configFile.Telemetry.Insecure { 28 | options = append(options, otlptracehttp.WithInsecure()) 29 | } 30 | 31 | exporter, err := otlptrace.New(context.TODO(), otlptracehttp.NewClient(options...)) 32 | if err != nil { 33 | return nil, fmt.Errorf("new exporter: %w", err) 34 | } 35 | 36 | var ( 37 | serviceName = constant.BuildServiceName() 38 | serviceVersion = constant.BuildServiceVersion() 39 | ) 40 | 41 | tracerProvider := tracer.NewTracerProvider( 42 | tracer.WithBatcher(exporter), 43 | tracer.WithResource(resource.NewWithAttributes( 44 | semconv.SchemaURL, 45 | semconv.ServiceNameKey.String(serviceName), 46 | semconv.ServiceVersionKey.String(serviceVersion), 47 | )), 48 | ) 49 | 50 | return tracerProvider, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/node_operation.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 8 | "github.com/labstack/echo/v4" 9 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 10 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 11 | "github.com/shopspring/decimal" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func (n *NTA) GetNodeOperationProfit(c echo.Context) error { 16 | var request nta.GetNodeOperationProfitRequest 17 | 18 | if err := c.Bind(&request); err != nil { 19 | return errorx.BadParamsError(c, fmt.Errorf("bind request: %w", err)) 20 | } 21 | 22 | if err := c.Validate(&request); err != nil { 23 | return errorx.ValidationFailedError(c, fmt.Errorf("validation failed: %w", err)) 24 | } 25 | 26 | node, err := n.stakingContract.GetNode(&bind.CallOpts{}, request.NodeAddress) 27 | if err != nil { 28 | zap.L().Error("get Node from rpc", zap.Error(err)) 29 | 30 | return errorx.InternalError(c) 31 | } 32 | 33 | data := nta.GetNodeOperationProfitResponse{ 34 | NodeAddress: request.NodeAddress, 35 | OperationPool: decimal.NewFromBigInt(node.OperationPoolTokens, 0), 36 | } 37 | 38 | changes, err := n.findNodeOperationProfitSnapshots(c.Request().Context(), request.NodeAddress, &data) 39 | if err != nil { 40 | zap.L().Error("find operator history profit snapshots", zap.Error(err)) 41 | 42 | return errorx.InternalError(c) 43 | } 44 | 45 | data.OneDay, data.OneWeek, data.OneMonth = changes[0], changes[1], changes[2] 46 | 47 | return c.JSON(http.StatusOK, nta.Response{ 48 | Data: data, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /internal/service/scheduler/enforcer/node_status/node_status.go: -------------------------------------------------------------------------------- 1 | package nodestatus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/internal/cronjob" 13 | "github.com/rss3-network/global-indexer/internal/service" 14 | "github.com/rss3-network/global-indexer/internal/service/hub/handler/dsl/enforcer" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | var _ service.Server = (*server)(nil) 19 | 20 | var Name = "node_status" 21 | 22 | type server struct { 23 | cronJob *cronjob.CronJob 24 | simpleEnforcer *enforcer.SimpleEnforcer 25 | } 26 | 27 | func (s *server) Name() string { 28 | return Name 29 | } 30 | 31 | func (s *server) Spec() string { 32 | return "0 */11 * * * *" 33 | } 34 | 35 | func (s *server) Run(ctx context.Context) error { 36 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 37 | if err := s.simpleEnforcer.MaintainNodeStatus(ctx); err != nil { 38 | zap.L().Error("maintain node_status error", zap.Error(err)) 39 | return 40 | } 41 | }) 42 | 43 | if err != nil { 44 | return fmt.Errorf("add maintain node status cron job: %w", err) 45 | } 46 | 47 | s.cronJob.Start() 48 | defer s.cronJob.Stop() 49 | 50 | stopChan := make(chan os.Signal, 1) 51 | 52 | signal.Notify(stopChan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 53 | <-stopChan 54 | 55 | return nil 56 | } 57 | 58 | func New(redis *redis.Client, simpleEnforcer *enforcer.SimpleEnforcer) service.Server { 59 | return &server{ 60 | cronJob: cronjob.New(redis, Name, 10*time.Second), 61 | simpleEnforcer: simpleEnforcer, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/service/hub/handler/dsl/enforcer/node_request.go: -------------------------------------------------------------------------------- 1 | package enforcer 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-version" 11 | "github.com/rss3-network/node/v2/schema/worker/decentralized" 12 | "github.com/rss3-network/protocol-go/schema/network" 13 | "github.com/rss3-network/protocol-go/schema/tag" 14 | ) 15 | 16 | // getNodeWorkerStatus retrieves the worker status for the node. 17 | func (e *SimpleEnforcer) getNodeWorkerStatus(ctx context.Context, versionStr, endpoint, accessToken string) (*WorkersStatusResponse, error) { 18 | curVersion, _ := version.NewVersion(versionStr) 19 | 20 | var prefix string 21 | if minVersion, _ := version.NewVersion("1.1.2"); curVersion.GreaterThanOrEqual(minVersion) { 22 | prefix = "operators/" 23 | } 24 | 25 | if !strings.HasSuffix(endpoint, "/") { 26 | endpoint += "/" 27 | } 28 | 29 | fullURL := endpoint + prefix + "workers_status" 30 | 31 | body, err := e.httpClient.FetchWithMethod(ctx, http.MethodGet, fullURL, accessToken, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | data, err := io.ReadAll(body) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | response := &WorkersStatusResponse{} 42 | 43 | if err = json.Unmarshal(data, response); err != nil { 44 | return nil, err 45 | } 46 | 47 | // Set the platform for the Farcaster network. 48 | for i, w := range response.Data.Decentralized { 49 | if w.Network == network.Farcaster { 50 | response.Data.Decentralized[i].Platform = decentralized.PlatformFarcaster 51 | response.Data.Decentralized[i].Tags = []tag.Tag{tag.Social} 52 | } 53 | } 54 | 55 | return response, nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/service/scheduler/enforcer/reliability_score/reliability_score.go: -------------------------------------------------------------------------------- 1 | package reliabilityscore 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/internal/cronjob" 13 | "github.com/rss3-network/global-indexer/internal/service" 14 | "github.com/rss3-network/global-indexer/internal/service/hub/handler/dsl/enforcer" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | var _ service.Server = (*server)(nil) 19 | 20 | var Name = "reliability_score" 21 | 22 | type server struct { 23 | cronJob *cronjob.CronJob 24 | simpleEnforcer *enforcer.SimpleEnforcer 25 | } 26 | 27 | func (s *server) Name() string { 28 | return Name 29 | } 30 | 31 | func (s *server) Spec() string { 32 | return "0 */5 * * * *" 33 | } 34 | 35 | func (s *server) Run(ctx context.Context) error { 36 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 37 | if err := s.simpleEnforcer.MaintainReliabilityScore(ctx); err != nil { 38 | zap.L().Error("maintain reliability_score error", zap.Error(err)) 39 | return 40 | } 41 | }) 42 | 43 | if err != nil { 44 | return fmt.Errorf("add maintain reliability score cron job: %w", err) 45 | } 46 | 47 | s.cronJob.Start() 48 | defer s.cronJob.Stop() 49 | 50 | stopChan := make(chan os.Signal, 1) 51 | 52 | signal.Notify(stopChan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 53 | <-stopChan 54 | 55 | return nil 56 | } 57 | 58 | func New(redis *redis.Client, simpleEnforcer *enforcer.SimpleEnforcer) service.Server { 59 | return &server{ 60 | cronJob: cronjob.New(redis, Name, 10*time.Second), 61 | simpleEnforcer: simpleEnforcer, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /schema/node_invalid_response.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | // NodeInvalidResponse records an alleged invalid response of a Node 10 | // A group of Nodes are selected as verifiers to verify the response returned by a Node 11 | // The response (with all responses from all verifiers) is saved in the database pending challenge by the penalized Node 12 | type NodeInvalidResponse struct { 13 | ID uint64 `json:"id"` 14 | EpochID uint64 `json:"epoch_id"` 15 | Type NodeInvalidResponseType `json:"type"` 16 | Request string `json:"request"` 17 | VerifierNodes []common.Address `json:"verifier_nodes"` 18 | VerifierResponse json.RawMessage `json:"verifier_response"` 19 | Node common.Address `json:"node"` 20 | Response json.RawMessage `json:"response"` 21 | CreatedAt int64 `json:"created_at"` 22 | } 23 | 24 | //go:generate go run --mod=mod github.com/dmarkham/enumer@v1.5.9 --values --type=NodeInvalidResponseType --linecomment --output node_invalid_response_type_string.go --json --yaml --sql 25 | type NodeInvalidResponseType int64 26 | 27 | const ( 28 | // NodeInvalidResponseTypeInconsistent when the Node's response differs from the majority of verifiers 29 | NodeInvalidResponseTypeInconsistent NodeInvalidResponseType = iota // inconsistent 30 | // NodeInvalidResponseTypeError when the Node returns an error 31 | NodeInvalidResponseTypeError // error 32 | // NodeInvalidResponseTypeOffline when the Node is offline 33 | NodeInvalidResponseTypeOffline // offline 34 | ) 35 | -------------------------------------------------------------------------------- /internal/provider/txmgr.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "time" 7 | 8 | gicrypto "github.com/rss3-network/global-indexer/common/crypto" 9 | "github.com/rss3-network/global-indexer/common/txmgr" 10 | "github.com/rss3-network/global-indexer/internal/client/ethereum" 11 | "github.com/rss3-network/global-indexer/internal/config" 12 | "github.com/rss3-network/global-indexer/internal/config/flag" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func ProvideTxManager(config *config.File, ethereumMultiChainClient *ethereum.MultiChainClient) (*txmgr.SimpleTxManager, error) { 17 | signerFactory, from, err := gicrypto.NewSignerFactory(config.Settler.PrivateKey, config.Settler.SignerEndpoint, config.Settler.WalletAddress) 18 | if err != nil { 19 | return nil, fmt.Errorf("create signer: %w", err) 20 | } 21 | 22 | defaultTxConfig := txmgr.Config{ 23 | ResubmissionTimeout: 20 * time.Second, 24 | FeeLimitMultiplier: 5, 25 | TxSendTimeout: 5 * time.Minute, 26 | TxNotInMempoolTimeout: 1 * time.Hour, 27 | NetworkTimeout: 5 * time.Minute, 28 | ReceiptQueryInterval: 500 * time.Millisecond, 29 | NumConfirmations: 5, 30 | SafeAbortNonceTooLowCount: 3, 31 | } 32 | 33 | chainID := new(big.Int).SetUint64(viper.GetUint64(flag.KeyChainIDL2)) 34 | 35 | ethereumClient, err := ethereumMultiChainClient.Get(chainID.Uint64()) 36 | if err != nil { 37 | return nil, fmt.Errorf("load l2 ethereum client: %w", err) 38 | } 39 | 40 | txManager, err := txmgr.NewSimpleTxManager(defaultTxConfig, chainID, nil, ethereumClient, from, signerFactory(chainID)) 41 | if err != nil { 42 | return nil, fmt.Errorf("create tx manager %w", err) 43 | } 44 | 45 | return txManager, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/service/indexer/internal/handler/l2/handler_chips.go: -------------------------------------------------------------------------------- 1 | package l2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/rss3-network/global-indexer/contract/l2" 9 | "github.com/rss3-network/global-indexer/internal/database" 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/attribute" 12 | ) 13 | 14 | func (h *handler) indexChipsLog(ctx context.Context, header *types.Header, transaction *types.Transaction, receipt *types.Receipt, log *types.Log, databaseTransaction database.Client) error { 15 | switch eventHash := log.Topics[0]; { 16 | case h.finalized && eventHash == l2.EventHashChipsTransfer: 17 | return h.indexChipsTransferLog(ctx, header, transaction, receipt, log, databaseTransaction) 18 | default: // Discard all unsupported events. 19 | return nil 20 | } 21 | } 22 | 23 | func (h *handler) indexChipsTransferLog(ctx context.Context, header *types.Header, transaction *types.Transaction, _ *types.Receipt, log *types.Log, databaseTransaction database.Client) error { 24 | ctx, span := otel.Tracer("").Start(ctx, "indexChipsTransferLog") 25 | defer span.End() 26 | 27 | span.SetAttributes( 28 | attribute.Int64("block.number", header.Number.Int64()), 29 | attribute.Stringer("block.hash", header.Hash()), 30 | attribute.Stringer("transaction.hash", transaction.Hash()), 31 | attribute.Int("log.index", int(log.Index)), 32 | ) 33 | 34 | event, err := h.contractChips.ParseTransfer(*log) 35 | if err != nil { 36 | return fmt.Errorf("parse Transfer event: %w", err) 37 | } 38 | 39 | if err := databaseTransaction.UpdateStakeChipsOwner(ctx, event.To, event.TokenId); err != nil { 40 | return fmt.Errorf("update stake chips owner: %w", err) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/client/ethereum/client.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/ethereum/go-ethereum/ethclient" 9 | "github.com/sourcegraph/conc/pool" 10 | ) 11 | 12 | type MultiChainClient struct { 13 | chainMap map[uint64]*ethclient.Client 14 | locker sync.RWMutex 15 | } 16 | 17 | func (m *MultiChainClient) Put(chainID uint64, ethereumClient *ethclient.Client) { 18 | m.locker.Lock() 19 | defer m.locker.Unlock() 20 | 21 | m.chainMap[chainID] = ethereumClient 22 | } 23 | 24 | func (m *MultiChainClient) Get(chainID uint64) (*ethclient.Client, error) { 25 | m.locker.RLock() 26 | defer m.locker.RUnlock() 27 | 28 | ethereumClient, found := m.chainMap[chainID] 29 | 30 | if !found { 31 | return nil, fmt.Errorf("client with chain id %d not found", chainID) 32 | } 33 | 34 | return ethereumClient, nil 35 | } 36 | 37 | func Dial(ctx context.Context, endpoints []string) (*MultiChainClient, error) { 38 | client := MultiChainClient{ 39 | chainMap: make(map[uint64]*ethclient.Client), 40 | } 41 | 42 | contextPool := pool.New().WithContext(ctx).WithFirstError().WithCancelOnError() 43 | 44 | for _, endpoint := range endpoints { 45 | endpoint := endpoint 46 | 47 | contextPool.Go(func(ctx context.Context) error { 48 | ethereumClient, err := ethclient.DialContext(ctx, endpoint) 49 | if err != nil { 50 | return fmt.Errorf("dial to endpoint: %w", err) 51 | } 52 | 53 | chainID, err := ethereumClient.ChainID(ctx) 54 | if err != nil { 55 | return fmt.Errorf("get chain id: %w", err) 56 | } 57 | 58 | client.Put(chainID.Uint64(), ethereumClient) 59 | 60 | return nil 61 | }) 62 | } 63 | 64 | if err := contextPool.Wait(); err != nil { 65 | return nil, err 66 | } 67 | 68 | return &client, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_apy_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | type NodeAPYSnapshot struct { 12 | ID uint64 `gorm:"column:id"` 13 | Date time.Time `gorm:"column:date"` 14 | EpochID uint64 `gorm:"column:epoch_id"` 15 | NodeAddress common.Address `gorm:"column:node_address"` 16 | APY decimal.Decimal `gorm:"column:apy"` 17 | CreatedAt time.Time `gorm:"column:created_at"` 18 | UpdatedAt time.Time `gorm:"column:updated_at"` 19 | } 20 | 21 | func (s *NodeAPYSnapshot) TableName() string { 22 | return "node.apy_snapshots" 23 | } 24 | 25 | func (s *NodeAPYSnapshot) Import(nodeAPYSnapshot *schema.NodeAPYSnapshot) error { 26 | s.Date = nodeAPYSnapshot.Date 27 | s.EpochID = nodeAPYSnapshot.EpochID 28 | s.NodeAddress = nodeAPYSnapshot.NodeAddress 29 | s.APY = nodeAPYSnapshot.APY 30 | 31 | return nil 32 | } 33 | 34 | func (s *NodeAPYSnapshot) Export() (*schema.NodeAPYSnapshot, error) { 35 | return &schema.NodeAPYSnapshot{ 36 | ID: s.ID, 37 | Date: s.Date, 38 | EpochID: s.EpochID, 39 | NodeAddress: s.NodeAddress, 40 | APY: s.APY, 41 | CreatedAt: s.CreatedAt, 42 | UpdatedAt: s.UpdatedAt, 43 | }, nil 44 | } 45 | 46 | type NodeAPYSnapshots []NodeAPYSnapshot 47 | 48 | func (s *NodeAPYSnapshots) Import(snapshots []*schema.NodeAPYSnapshot) error { 49 | for _, snapshot := range snapshots { 50 | var imported NodeAPYSnapshot 51 | 52 | if err := imported.Import(snapshot); err != nil { 53 | return err 54 | } 55 | 56 | *s = append(*s, imported) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /deploy/config.example.yaml: -------------------------------------------------------------------------------- 1 | environment: development 2 | 3 | database: 4 | driver: postgres 5 | partition: true 6 | uri: postgres://postgres:password@localhost:5432/postgres 7 | 8 | redis: 9 | uri: redis://localhost:6379/0 10 | 11 | rss3_chain: 12 | endpoint_l1: https://rpc.ankr.com/eth_sepolia 13 | endpoint_l2: https://rpc.testnet.rss3.io 14 | block_threads_l1: 20 15 | block_threads_l2: 100 16 | 17 | settler: 18 | private_key: 19 | wallet_address: 20 | signer_endpoint: http://localhost:3000 21 | epoch_interval_in_hours: 18 22 | gas_limit: 3000000 23 | batch_size: 200 24 | production_start_epoch: 227 25 | grace_period_epochs: 28 26 | 27 | rewards: 28 | operation_rewards: 12328 # 30000000 / 486.6666666666667 * 0.2 29 | operation_score: 30 | distribution: 31 | weight: 0.6 32 | weight_invalid: 0.5 33 | data: 34 | weight: 0.3 35 | weight_network: 0.3 36 | weight_indexer: 0.6 37 | weight_activity: 0.1 38 | stability: 39 | weight: 0.1 40 | weight_uptime: 0.7 41 | weight_version: 0.3 42 | 43 | 44 | active_scores: 45 | gini_coefficient: 2 46 | staker_factor: 0.05 47 | epoch_limit: 10 48 | 49 | geo_ip: 50 | account: 51 | license_key: 52 | 53 | rpc: 54 | network: 55 | ethereum: 56 | endpoint: https://rpc.ankr.com/eth 57 | crossbell: 58 | endpoint: https://rpc.crossbell.io 59 | polygon: 60 | endpoint: https://rpc.ankr.com/polygon 61 | farcaster: 62 | endpoint: https://nemes.farcaster.xyz:2281 63 | api_key: 64 | 65 | telemetry: 66 | endpoint: localhost:4318 67 | insecure: true 68 | 69 | distributor: 70 | max_demotion_count: -1 71 | qualified_node_count: 3 72 | verification_count: 3 73 | tolerance_seconds: 1200 74 | 75 | token_price_api: 76 | endpoint: 77 | auth_token: 78 | 79 | -------------------------------------------------------------------------------- /common/signer/client.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/ethereum/go-ethereum/rpc" 14 | ) 15 | 16 | type Client struct { 17 | client *rpc.Client 18 | } 19 | 20 | func NewSignerClient(endpoint string) (*Client, error) { 21 | var httpClient *http.Client 22 | 23 | rpcClient, err := rpc.DialOptions(context.Background(), endpoint, rpc.WithHTTPClient(httpClient)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | signer := &Client{client: rpcClient} 29 | // Check if reachable 30 | res, err := signer.pingVersion() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if res != "ok" { 36 | return nil, fmt.Errorf("signer service unreachable") 37 | } 38 | 39 | return signer, nil 40 | } 41 | 42 | func (s *Client) pingVersion() (string, error) { 43 | var v string 44 | 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) 46 | 47 | defer cancel() 48 | 49 | if err := s.client.CallContext(ctx, &v, "health_status"); err != nil { 50 | return "", err 51 | } 52 | 53 | return v, nil 54 | } 55 | 56 | func (s *Client) SignTransaction(ctx context.Context, chainID *big.Int, from common.Address, tx *types.Transaction) (*types.Transaction, error) { 57 | args := NewTransactionArgsFromTransaction(chainID, from, tx) 58 | 59 | var result hexutil.Bytes 60 | if err := s.client.CallContext(ctx, &result, "eth_signTransaction", args); err != nil { 61 | return nil, fmt.Errorf("eth_signTransaction failed: %w", err) 62 | } 63 | 64 | signed := &types.Transaction{} 65 | if err := signed.UnmarshalBinary(result); err != nil { 66 | return nil, err 67 | } 68 | 69 | return signed, nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/epoch_trigger.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | ) 10 | 11 | type EpochTrigger struct { 12 | TransactionHash string `gorm:"column:transaction_hash"` 13 | EpochID uint64 `gorm:"column:epoch_id"` 14 | Data json.RawMessage `gorm:"column:data"` 15 | CreatedAt time.Time `gorm:"column:created_at"` 16 | UpdatedAt time.Time `gorm:"column:updated_at"` 17 | } 18 | 19 | func (e *EpochTrigger) TableName() string { 20 | return "epoch_trigger" 21 | } 22 | 23 | func (e *EpochTrigger) Import(epochTrigger *schema.EpochTrigger) (err error) { 24 | e.TransactionHash = epochTrigger.TransactionHash.String() 25 | e.EpochID = epochTrigger.EpochID 26 | e.CreatedAt = epochTrigger.CreatedAt 27 | e.UpdatedAt = epochTrigger.UpdatedAt 28 | 29 | e.Data, err = json.Marshal(epochTrigger.Data) 30 | 31 | return err 32 | } 33 | 34 | func (e *EpochTrigger) Export() (*schema.EpochTrigger, error) { 35 | var data schema.SettlementData 36 | if err := json.Unmarshal(e.Data, &data); err != nil { 37 | return nil, err 38 | } 39 | 40 | return &schema.EpochTrigger{ 41 | TransactionHash: common.HexToHash(e.TransactionHash), 42 | EpochID: e.EpochID, 43 | Data: data, 44 | CreatedAt: e.CreatedAt, 45 | UpdatedAt: e.UpdatedAt, 46 | }, nil 47 | } 48 | 49 | type EpochTriggers []*EpochTrigger 50 | 51 | func (e EpochTriggers) Export() ([]*schema.EpochTrigger, error) { 52 | result := make([]*schema.EpochTrigger, 0) 53 | 54 | for _, epochTrigger := range e { 55 | exported, err := epochTrigger.Export() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | result = append(result, exported) 61 | } 62 | 63 | return result, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/epoch_apy_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rss3-network/global-indexer/schema" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | type EpochAPYSnapshot struct { 11 | EpochID uint64 `gorm:"column:epoch_id"` 12 | Date time.Time `gorm:"column:date"` 13 | APY decimal.Decimal `gorm:"column:apy"` 14 | CreatedAt time.Time `gorm:"column:created_at"` 15 | UpdatedAt time.Time `gorm:"column:updated_at"` 16 | } 17 | 18 | func (e *EpochAPYSnapshot) TableName() string { 19 | return "epoch.apy_snapshots" 20 | } 21 | 22 | func (e *EpochAPYSnapshot) Import(epochAPYSnapshot *schema.EpochAPYSnapshot) error { 23 | e.EpochID = epochAPYSnapshot.EpochID 24 | e.Date = epochAPYSnapshot.Date 25 | e.APY = epochAPYSnapshot.APY 26 | 27 | return nil 28 | } 29 | 30 | func (e *EpochAPYSnapshot) Export() (*schema.EpochAPYSnapshot, error) { 31 | return &schema.EpochAPYSnapshot{ 32 | EpochID: e.EpochID, 33 | Date: e.Date, 34 | APY: e.APY, 35 | CreatedAt: e.CreatedAt, 36 | UpdatedAt: e.UpdatedAt, 37 | }, nil 38 | } 39 | 40 | type EpochAPYSnapshots []EpochAPYSnapshot 41 | 42 | func (e *EpochAPYSnapshots) Import(snapshots []*schema.EpochAPYSnapshot) error { 43 | for _, snapshot := range snapshots { 44 | var imported EpochAPYSnapshot 45 | 46 | if err := imported.Import(snapshot); err != nil { 47 | return err 48 | } 49 | 50 | *e = append(*e, imported) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (e *EpochAPYSnapshots) Export() ([]*schema.EpochAPYSnapshot, error) { 57 | snapshots := make([]*schema.EpochAPYSnapshot, 0, len(*e)) 58 | 59 | for _, snapshot := range *e { 60 | exported, err := snapshot.Export() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | snapshots = append(snapshots, exported) 66 | } 67 | 68 | return snapshots, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/node_event.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/creasty/defaults" 9 | "github.com/labstack/echo/v4" 10 | "github.com/rss3-network/global-indexer/internal/database" 11 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 12 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 13 | "github.com/rss3-network/global-indexer/schema" 14 | "github.com/samber/lo" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | func (n *NTA) GetNodeEvents(c echo.Context) error { 19 | var request nta.NodeEventsRequest 20 | 21 | if err := c.Bind(&request); err != nil { 22 | return errorx.BadParamsError(c, fmt.Errorf("bind request: %w", err)) 23 | } 24 | 25 | if err := defaults.Set(&request); err != nil { 26 | return errorx.BadRequestError(c, fmt.Errorf("set default failed: %w", err)) 27 | } 28 | 29 | if err := c.Validate(&request); err != nil { 30 | return errorx.ValidationFailedError(c, fmt.Errorf("validation failed: %w", err)) 31 | } 32 | 33 | events, err := n.databaseClient.FindNodeEvents(c.Request().Context(), &schema.NodeEventsQuery{ 34 | NodeAddress: lo.ToPtr(request.NodeAddress), 35 | Cursor: request.Cursor, 36 | Limit: lo.ToPtr(request.Limit), 37 | }) 38 | if err != nil { 39 | if errors.Is(err, database.ErrorRowNotFound) { 40 | return c.NoContent(http.StatusNotFound) 41 | } 42 | 43 | zap.L().Error("get Node events failed", zap.Error(err)) 44 | 45 | return errorx.InternalError(c) 46 | } 47 | 48 | var cursor string 49 | 50 | if len(events) > 0 && len(events) == request.Limit { 51 | last, _ := lo.Last(events) 52 | cursor = fmt.Sprintf("%s:%d:%d", last.TransactionHash, last.TransactionIndex, last.LogIndex) 53 | } 54 | 55 | return c.JSON(http.StatusOK, nta.Response{ 56 | Data: nta.NewNodeEvents(events), 57 | Cursor: cursor, 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /internal/service/scheduler/server.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redis/go-redis/v9" 7 | "github.com/rss3-network/global-indexer/common/httputil" 8 | "github.com/rss3-network/global-indexer/common/txmgr" 9 | "github.com/rss3-network/global-indexer/internal/client/ethereum" 10 | "github.com/rss3-network/global-indexer/internal/config" 11 | "github.com/rss3-network/global-indexer/internal/config/flag" 12 | "github.com/rss3-network/global-indexer/internal/database" 13 | "github.com/rss3-network/global-indexer/internal/service" 14 | "github.com/rss3-network/global-indexer/internal/service/scheduler/detector" 15 | "github.com/rss3-network/global-indexer/internal/service/scheduler/enforcer" 16 | "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot" 17 | "github.com/rss3-network/global-indexer/internal/service/scheduler/taxer" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | // NewServer creates a new scheduler server that executes cron jobs. 22 | func NewServer(databaseClient database.Client, redis *redis.Client, ethereumMultiChainClient *ethereum.MultiChainClient, httpClient httputil.Client, config *config.File, txManager *txmgr.SimpleTxManager) (service.Server, error) { 23 | ethereumClient, err := ethereumMultiChainClient.Get(viper.GetUint64(flag.KeyChainIDL2)) 24 | if err != nil { 25 | return nil, fmt.Errorf("get ethereum client: %w", err) 26 | } 27 | 28 | switch server := viper.GetString(flag.KeyServer); server { 29 | case detector.Name: 30 | return detector.New(databaseClient, redis) 31 | case enforcer.Name: 32 | return enforcer.New(databaseClient, redis, ethereumClient, httpClient, config, txManager) 33 | case snapshot.Name: 34 | return snapshot.New(databaseClient, redis, ethereumClient) 35 | case taxer.Name: 36 | return taxer.New(databaseClient, redis, ethereumClient, config, txManager) 37 | default: 38 | return nil, fmt.Errorf("unknown scheduler server: %s", server) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/snapshot.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/samber/lo" 9 | ) 10 | 11 | type GetStakerProfitSnapshotsRequest struct { 12 | StakerAddress common.Address `query:"staker_address" validate:"required"` 13 | Limit *int `query:"limit"` 14 | Cursor *string `query:"cursor"` 15 | BeforeDate *time.Time `query:"before_date"` 16 | AfterDate *time.Time `query:"after_date"` 17 | } 18 | 19 | type GetNodeOperationProfitSnapshotsRequest struct { 20 | NodeAddress common.Address `query:"node_address" validate:"required"` 21 | Limit *int `query:"limit"` 22 | Cursor *string `query:"cursor"` 23 | BeforeDate *time.Time `query:"before_date"` 24 | AfterDate *time.Time `query:"after_date"` 25 | } 26 | 27 | type GetNodeCountSnapshotsResponseData []*CountSnapshot 28 | 29 | type GetStakerCountSnapshotsResponseData []*CountSnapshot 30 | 31 | type GetOperatorProfitsSnapshotsResponseData []*schema.OperatorProfitSnapshot 32 | 33 | type CountSnapshot struct { 34 | Date string `json:"date"` 35 | Count uint64 `json:"count"` 36 | } 37 | 38 | func NewNodeCountSnapshots(nodeSnapshots []*schema.NodeSnapshot) GetNodeCountSnapshotsResponseData { 39 | return lo.Map(nodeSnapshots, func(nodeSnapshot *schema.NodeSnapshot, _ int) *CountSnapshot { 40 | return &CountSnapshot{ 41 | Date: nodeSnapshot.Date.Format(time.DateOnly), 42 | Count: uint64(nodeSnapshot.Count), 43 | } 44 | }) 45 | } 46 | 47 | func NewStakerCountSnapshots(stakeSnapshots []*schema.StakerCountSnapshot) GetStakerCountSnapshotsResponseData { 48 | return lo.Map(stakeSnapshots, func(stakeSnapshot *schema.StakerCountSnapshot, _ int) *CountSnapshot { 49 | return &CountSnapshot{ 50 | Date: stakeSnapshot.Date.Format(time.DateOnly), 51 | Count: uint64(stakeSnapshot.Count), 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /internal/service/scheduler/snapshot/node_count/node_count.go: -------------------------------------------------------------------------------- 1 | package nodecount 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/internal/cronjob" 13 | "github.com/rss3-network/global-indexer/internal/database" 14 | "github.com/rss3-network/global-indexer/internal/service" 15 | "github.com/rss3-network/global-indexer/schema" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | var ( 20 | Name = "node_count" 21 | Timeout = 10 * time.Second 22 | ) 23 | 24 | var _ service.Server = (*server)(nil) 25 | 26 | type server struct { 27 | cronJob *cronjob.CronJob 28 | databaseClient database.Client 29 | redisClient *redis.Client 30 | } 31 | 32 | func (s *server) Name() string { 33 | return Name 34 | } 35 | 36 | func (s *server) Spec() string { 37 | return "0 0 0 * * *" 38 | } 39 | 40 | func (s *server) Run(ctx context.Context) error { 41 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 42 | year, month, day := time.Now().UTC().Date() 43 | date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) 44 | 45 | nodeSnapshot := schema.NodeSnapshot{ 46 | Date: date, 47 | } 48 | 49 | if err := s.databaseClient.SaveNodeCountSnapshot(ctx, &nodeSnapshot); err != nil { 50 | zap.L().Error("save Node count snapshot error", zap.Error(err)) 51 | 52 | return 53 | } 54 | }) 55 | if err != nil { 56 | return fmt.Errorf("add node count cron job: %w", err) 57 | } 58 | 59 | s.cronJob.Start() 60 | defer s.cronJob.Stop() 61 | 62 | stopchan := make(chan os.Signal, 1) 63 | 64 | signal.Notify(stopchan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 65 | <-stopchan 66 | 67 | return nil 68 | } 69 | 70 | func New(databaseClient database.Client, redis *redis.Client) service.Server { 71 | return &server{ 72 | cronJob: cronjob.New(redis, Name, Timeout), 73 | databaseClient: databaseClient, 74 | redisClient: redis, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /schema/stake_event.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | "time" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | type StakeEventType string 12 | 13 | const ( 14 | StakeEventTypeDepositDeposited StakeEventType = "deposited" 15 | 16 | StakeEventTypeWithdrawRequested StakeEventType = "withdraw_requested" 17 | StakeEventTypeWithdrawClaimed StakeEventType = "withdraw_claimed" 18 | 19 | StakeEventTypeStakeStaked StakeEventType = "staked" 20 | 21 | StakeEventTypeChipsMerged = "merged" 22 | 23 | StakeEventTypeUnstakeRequested StakeEventType = "unstake_requested" 24 | StakeEventTypeUnstakeClaimed StakeEventType = "unstake_claimed" 25 | ) 26 | 27 | type StakeEventImporter interface { 28 | Import(stakeEvent StakeEvent) error 29 | } 30 | 31 | type StakeEventExporter interface { 32 | Export() (*StakeEvent, error) 33 | } 34 | 35 | type StakeEventTransformer interface { 36 | StakeEventImporter 37 | StakeEventExporter 38 | } 39 | 40 | type StakeEvent struct { 41 | ID common.Hash `json:"id"` 42 | Type StakeEventType `json:"type"` 43 | TransactionHash common.Hash `json:"transaction_hash"` 44 | TransactionIndex uint `json:"transaction_index"` 45 | TransactionStatus uint64 `json:"transaction_status"` 46 | LogIndex uint `json:"log_index"` 47 | Metadata json.RawMessage `json:"metadata"` 48 | BlockHash common.Hash `json:"block_hash"` 49 | BlockNumber *big.Int `json:"block_number"` 50 | BlockTimestamp time.Time `json:"block_timestamp"` 51 | Finalized bool 52 | } 53 | 54 | type StakeEventQuery struct { 55 | ID *common.Hash `query:"id"` 56 | } 57 | 58 | type StakeEventsQuery struct { 59 | IDs []common.Hash `query:"ids"` 60 | } 61 | 62 | type StakeEventChipsMergedMetadata struct { 63 | BurnedTokenIDs []*big.Int `json:"burned_token_ids"` 64 | NewTokenID *big.Int `json:"new_token_id"` 65 | } 66 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/stake_chip.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | "github.com/shopspring/decimal" 10 | gorm "gorm.io/gorm/schema" 11 | ) 12 | 13 | var ( 14 | _ gorm.Tabler = (*StakeChip)(nil) 15 | _ schema.StakeChipTransformer = (*StakeChip)(nil) 16 | ) 17 | 18 | type StakeChip struct { 19 | ID decimal.Decimal `gorm:"column:id"` 20 | Owner string `gorm:"column:owner"` 21 | Node string `gorm:"column:node"` 22 | Value decimal.Decimal `gorm:"column:value"` 23 | Metadata json.RawMessage `gorm:"column:metadata"` 24 | BlockNumber decimal.Decimal `gorm:"column:block_number"` 25 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 26 | Finalized bool `gorm:"column:finalized"` 27 | } 28 | 29 | func (s *StakeChip) TableName() string { 30 | return "stake.chips" 31 | } 32 | 33 | func (s *StakeChip) Import(stakeChip schema.StakeChip) error { 34 | s.ID = decimal.NewFromBigInt(stakeChip.ID, 0) 35 | s.Owner = stakeChip.Owner.String() 36 | s.Node = stakeChip.Node.String() 37 | s.Value = stakeChip.Value 38 | s.Metadata = stakeChip.Metadata 39 | s.BlockNumber = decimal.NewFromBigInt(stakeChip.BlockNumber, 0) 40 | s.BlockTimestamp = time.Unix(int64(stakeChip.BlockTimestamp), 0) 41 | s.Finalized = stakeChip.Finalized 42 | 43 | return nil 44 | } 45 | 46 | func (s *StakeChip) Export() (*schema.StakeChip, error) { 47 | stakeChip := schema.StakeChip{ 48 | ID: s.ID.BigInt(), 49 | Owner: common.HexToAddress(s.Owner), 50 | Node: common.HexToAddress(s.Node), 51 | Value: s.Value, 52 | Metadata: s.Metadata, 53 | BlockNumber: s.BlockNumber.BigInt(), 54 | BlockTimestamp: uint64(s.BlockTimestamp.Unix()), 55 | Finalized: s.Finalized, 56 | } 57 | 58 | return &stakeChip, nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/service/scheduler/snapshot/staker_count/staker_count.go: -------------------------------------------------------------------------------- 1 | package stakercount 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/internal/cronjob" 13 | "github.com/rss3-network/global-indexer/internal/database" 14 | "github.com/rss3-network/global-indexer/internal/service" 15 | "github.com/rss3-network/global-indexer/schema" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | var ( 20 | Name = "staker_count" 21 | Timeout = 10 * time.Second 22 | ) 23 | 24 | var _ service.Server = (*server)(nil) 25 | 26 | type server struct { 27 | cronJob *cronjob.CronJob 28 | databaseClient database.Client 29 | redisClient *redis.Client 30 | } 31 | 32 | func (s *server) Name() string { 33 | return Name 34 | } 35 | 36 | func (s *server) Spec() string { 37 | return "0 0 0 * * *" 38 | } 39 | 40 | func (s *server) Run(ctx context.Context) error { 41 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 42 | year, month, day := time.Now().UTC().Date() 43 | date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) 44 | 45 | stakeSnapshot := schema.StakerCountSnapshot{ 46 | Date: date, 47 | } 48 | 49 | if err := s.databaseClient.SaveStakerCountSnapshot(ctx, &stakeSnapshot); err != nil { 50 | zap.L().Error("save staker_count snapshot error", zap.Error(err)) 51 | 52 | return 53 | } 54 | }) 55 | if err != nil { 56 | return fmt.Errorf("add staker_count cron job: %w", err) 57 | } 58 | 59 | s.cronJob.Start() 60 | defer s.cronJob.Stop() 61 | 62 | stopchan := make(chan os.Signal, 1) 63 | 64 | signal.Notify(stopchan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 65 | <-stopchan 66 | 67 | return nil 68 | } 69 | 70 | func New(databaseClient database.Client, redis *redis.Client) service.Server { 71 | return &server{ 72 | cronJob: cronjob.New(redis, Name, Timeout), 73 | databaseClient: databaseClient, 74 | redisClient: redis, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contract/multicall3/contract.go: -------------------------------------------------------------------------------- 1 | package multicall3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/ethereum/go-ethereum" 9 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/samber/lo" 12 | "github.com/sourcegraph/conc/pool" 13 | ) 14 | 15 | // Multicall https://github.com/mds1/multicall 16 | // https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11 17 | //go:generate go run -mod=mod github.com/ethereum/go-ethereum/cmd/abigen@v1.13.5 --abi ./abi/Multicall3.abi --pkg multicall3 --type Multicall3 --out contract_multicall3.go 18 | 19 | var ( 20 | AddressMulticall3 = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") 21 | ChainIDRSS3Mainnet uint64 = 12553 22 | ChainIDRSS3Testnet uint64 = 2331 23 | ) 24 | 25 | func Aggregate3(ctx context.Context, chainID uint64, calls []Multicall3Call3, blockNumber *big.Int, contractBackend bind.ContractCaller) ([]*Multicall3Result, error) { 26 | if !lo.Contains([]uint64{ChainIDRSS3Mainnet, ChainIDRSS3Testnet}, chainID) { 27 | return nil, fmt.Errorf("unsupported chain id: %d", chainID) 28 | } 29 | 30 | errorPool := pool.New().WithContext(ctx).WithCancelOnError() 31 | 32 | results := make([]*Multicall3Result, len(calls)) 33 | 34 | for index, call := range calls { 35 | index := index 36 | call := call 37 | 38 | errorPool.Go(func(ctx context.Context) error { 39 | message := ethereum.CallMsg{ 40 | To: lo.ToPtr(call.Target), 41 | Data: call.CallData, 42 | } 43 | 44 | data, err := contractBackend.CallContract(ctx, message, blockNumber) 45 | if err != nil && !call.AllowFailure { 46 | return err 47 | } 48 | 49 | result := Multicall3Result{ 50 | Success: err == nil, 51 | ReturnData: data, 52 | } 53 | 54 | results[index] = &result 55 | 56 | return nil 57 | }) 58 | } 59 | 60 | if err := errorPool.Wait(); err != nil { 61 | return nil, err 62 | } 63 | 64 | return results, nil 65 | } 66 | -------------------------------------------------------------------------------- /schema/stake_transaction.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | type StakeTransactionType string 12 | 13 | const ( 14 | StakeTransactionTypeDeposit StakeTransactionType = "deposit" 15 | StakeTransactionTypeWithdraw StakeTransactionType = "withdraw" 16 | StakeTransactionTypeStake StakeTransactionType = "stake" 17 | StakeTransactionTypeUnstake StakeTransactionType = "unstake" 18 | StakeTransactionTypeMergeChips StakeTransactionType = "merge_chips" 19 | ) 20 | 21 | type StakeTransactionImporter interface { 22 | Import(stakeTransaction StakeTransaction) error 23 | } 24 | 25 | type StakeTransactionExporter interface { 26 | Export() (*StakeTransaction, error) 27 | } 28 | 29 | type StakeTransactionTransformer interface { 30 | StakeTransactionImporter 31 | StakeTransactionExporter 32 | } 33 | 34 | type StakeTransaction struct { 35 | ID common.Hash 36 | Type StakeTransactionType 37 | User common.Address 38 | Node common.Address 39 | Value *big.Int 40 | ChipIDs []*big.Int 41 | BlockTimestamp time.Time 42 | BlockNumber uint64 43 | TransactionIndex uint 44 | Finalized bool 45 | } 46 | 47 | type StakeTransactionQuery struct { 48 | ID *common.Hash 49 | User *common.Address 50 | Node *common.Address 51 | Address *common.Address 52 | Type *StakeTransactionType 53 | } 54 | 55 | type StakeTransactionsQuery struct { 56 | Cursor *common.Hash 57 | IDs []common.Hash 58 | User *common.Address 59 | Node *common.Address 60 | Address *common.Address 61 | Type *StakeTransactionType 62 | BlockTimestamp *time.Time 63 | Pending *bool 64 | Limit int 65 | Order string 66 | Finalized *bool 67 | } 68 | 69 | type StakeRecentCount struct { 70 | StakerCount uint64 71 | StakeValue decimal.Decimal 72 | } 73 | -------------------------------------------------------------------------------- /internal/service/scheduler/detector/detector.go: -------------------------------------------------------------------------------- 1 | package detector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/internal/cronjob" 13 | "github.com/rss3-network/global-indexer/internal/database" 14 | "github.com/rss3-network/global-indexer/internal/service" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | var _ service.Server = (*server)(nil) 19 | 20 | var Name = "detector" 21 | 22 | type server struct { 23 | cronJob *cronjob.CronJob 24 | databaseClient database.Client 25 | } 26 | 27 | func (s *server) Name() string { 28 | return Name 29 | } 30 | 31 | func (s *server) Spec() string { 32 | return "*/5 * * * * *" 33 | } 34 | 35 | func (s *server) Run(ctx context.Context) error { 36 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 37 | if err := s.updateNodeActivity(ctx); err != nil { 38 | zap.L().Error("detect node activity error", zap.Error(err)) 39 | return 40 | } 41 | }) 42 | if err != nil { 43 | return fmt.Errorf("add detector cron job: %w", err) 44 | } 45 | 46 | s.cronJob.Start() 47 | defer s.cronJob.Stop() 48 | 49 | stopchan := make(chan os.Signal, 1) 50 | 51 | signal.Notify(stopchan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 52 | <-stopchan 53 | 54 | return nil 55 | } 56 | 57 | func (s *server) updateNodeActivity(ctx context.Context) error { 58 | timeout := time.Now().Add(-5 * time.Minute) 59 | 60 | if err := s.databaseClient.UpdateNodesStatusOffline(ctx, timeout.Unix()); err != nil { 61 | zap.L().Error("update node activity error", zap.Error(err), zap.String("timeout", timeout.String())) 62 | 63 | return fmt.Errorf("update node activity: %w", err) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func New(databaseClient database.Client, redis *redis.Client) (service.Server, error) { 70 | instance := server{ 71 | databaseClient: databaseClient, 72 | cronJob: cronjob.New(redis, Name, 10*time.Second), 73 | } 74 | 75 | return &instance, nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/node_hide_tax_rate.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/labstack/echo/v4" 10 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 11 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func (n *NTA) PostNodeHideTaxRate(c echo.Context) error { 16 | var request nta.NodeHideTaxRateRequest 17 | 18 | if err := c.Bind(&request); err != nil { 19 | return errorx.BadParamsError(c, fmt.Errorf("bind request: %w", err)) 20 | } 21 | 22 | if err := c.Validate(&request); err != nil { 23 | return errorx.ValidationFailedError(c, fmt.Errorf("validation failed: %w", err)) 24 | } 25 | 26 | message := fmt.Sprintf(hideTaxRateMessage, strings.ToLower(request.NodeAddress.String())) 27 | 28 | if err := n.checkSignature(c.Request().Context(), request.NodeAddress, message, request.Signature); err != nil { 29 | return errorx.ValidationFailedError(c, fmt.Errorf("check signature: %w", err)) 30 | } 31 | 32 | // Cache the hide tax rate status 33 | if err := n.cacheClient.Set(c.Request().Context(), n.buildNodeHideTaxRateKey(request.NodeAddress), true, 0); err != nil { 34 | zap.L().Error("cache hide tax value", zap.Error(err)) 35 | 36 | return errorx.InternalError(c) 37 | } 38 | 39 | // If the Node exists and is not a public good Node, update the hide tax rate status 40 | if node, err := n.getNode(c.Request().Context(), request.NodeAddress); err == nil && !node.IsPublicGood { 41 | if err := n.databaseClient.UpdateNodesHideTaxRate(c.Request().Context(), request.NodeAddress, true); err != nil { 42 | zap.L().Error("update node hide tax rate", zap.Error(err)) 43 | 44 | return errorx.InternalError(c) 45 | } 46 | } 47 | 48 | return c.NoContent(http.StatusOK) 49 | } 50 | 51 | func (n *NTA) buildNodeHideTaxRateKey(address common.Address) string { 52 | return fmt.Sprintf("node::%s::hideTaxRate", strings.ToLower(address.String())) 53 | } 54 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/client_average_tax_submission.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rss3-network/global-indexer/internal/database/dialer/postgres/table" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "go.uber.org/zap" 9 | "gorm.io/gorm/clause" 10 | ) 11 | 12 | // SaveAverageTaxSubmission Save records of average tax submissions 13 | func (c *client) SaveAverageTaxSubmission(ctx context.Context, submission *schema.AverageTaxRateSubmission) error { 14 | var data table.AverageTaxRateSubmission 15 | if err := data.Import(submission); err != nil { 16 | zap.L().Error("import average tax submission", zap.Error(err), zap.Any("submission", submission)) 17 | 18 | return err 19 | } 20 | 21 | onConflict := clause.OnConflict{ 22 | Columns: []clause.Column{ 23 | { 24 | Name: "epoch_id", 25 | }, 26 | }, 27 | UpdateAll: true, 28 | } 29 | 30 | if err := c.database.WithContext(ctx).Clauses(onConflict).Create(&data).Error; err != nil { 31 | zap.L().Error("insert average tax submission", zap.Error(err), zap.Any("submission", submission)) 32 | 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // FindAverageTaxSubmissions Find records of average tax submissions 40 | func (c *client) FindAverageTaxSubmissions(ctx context.Context, query schema.AverageTaxRateSubmissionQuery) ([]*schema.AverageTaxRateSubmission, error) { 41 | databaseStatement := c.database.WithContext(ctx).Table((*table.AverageTaxRateSubmission).TableName(nil)) 42 | 43 | if query.EpochID != nil { 44 | databaseStatement = databaseStatement.Where("epoch_id = ?", *query.EpochID) 45 | } 46 | 47 | if query.Limit != nil { 48 | databaseStatement = databaseStatement.Limit(*query.Limit) 49 | } 50 | 51 | var submissions table.AverageTaxSubmissions 52 | 53 | if err := databaseStatement.Order("epoch_id DESC").Find(&submissions).Error; err != nil { 54 | zap.L().Error("find average tax submissions", zap.Error(err), zap.Any("query", query)) 55 | 56 | return nil, err 57 | } 58 | 59 | return submissions.Export() 60 | } 61 | -------------------------------------------------------------------------------- /schema/node_stat.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type Stat struct { 10 | Address common.Address `json:"address"` 11 | Endpoint string `json:"-"` 12 | AccessToken string `json:"-"` 13 | Score float64 `json:"score"` 14 | IsPublicGood bool `json:"is_public_good"` 15 | IsFullNode bool `json:"is_full_node"` 16 | IsRssNode bool `json:"is_rss_node"` 17 | IsAINode bool `json:"is_ai_node"` 18 | Staking float64 `json:"staking"` 19 | Epoch int64 `json:"epoch"` 20 | TotalRequest int64 `json:"total_request"` 21 | EpochRequest int64 `json:"epoch_request"` 22 | EpochInvalidRequest int64 `json:"epoch_invalid_request"` 23 | DecentralizedNetwork int `json:"decentralized_network"` 24 | FederatedNetwork int `json:"federated_network"` 25 | Indexer int `json:"indexer"` 26 | ResetAt time.Time `json:"reset_at"` 27 | 28 | Status NodeStatus `json:"-"` 29 | HearBeat NodeStatus `json:"-"` 30 | Version string `json:"-"` 31 | } 32 | 33 | type StatQuery struct { 34 | Address *common.Address `query:"address" form:"address,omitempty"` 35 | Addresses []common.Address `query:"addresses" form:"addresses,omitempty"` 36 | IsFullNode *bool `query:"is_full_node" form:"is_full_node,omitempty"` 37 | IsRssNode *bool `query:"is_rss_node" form:"is_rss_node,omitempty"` 38 | IsAINode *bool `query:"is_ai_node" form:"is_ai_node,omitempty"` 39 | PointsOrder *string `query:"points_order" form:"points_order,omitempty"` 40 | ValidRequest *int `query:"valid_request" form:"valid_request,omitempty"` 41 | Limit *int `query:"limit" form:"limit,omitempty"` 42 | Cursor *string `query:"cursor" form:"cursor,omitempty"` 43 | } 44 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/node_event.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/rss3-network/global-indexer/schema" 6 | ) 7 | 8 | type NodeEventsRequest struct { 9 | NodeAddress common.Address `param:"node_address" validate:"required"` 10 | Cursor *string `query:"cursor"` 11 | Limit int `query:"limit" validate:"min=1,max=100" default:"20"` 12 | } 13 | 14 | type NodeEventResponseData *NodeEvent 15 | 16 | type NodeEventsResponseData []*NodeEvent 17 | 18 | type NodeEvent struct { 19 | Transaction TransactionEventTransaction `json:"transaction"` 20 | Block TransactionEventBlock `json:"block"` 21 | AddressFrom common.Address `json:"address_from"` 22 | AddressTo common.Address `json:"address_to"` 23 | NodeID uint64 `json:"node_id"` 24 | Type schema.NodeEventType `json:"type"` 25 | LogIndex uint `json:"log_index"` 26 | ChainID uint64 `json:"chain_id"` 27 | Metadata schema.NodeEventMetadata `json:"metadata"` 28 | Finalized bool `json:"finalized"` 29 | } 30 | 31 | func NewNodeEvent(event *schema.NodeEvent) NodeEventResponseData { 32 | return &NodeEvent{ 33 | Transaction: TransactionEventTransaction{ 34 | Hash: event.TransactionHash, 35 | Index: event.TransactionIndex, 36 | }, 37 | Block: TransactionEventBlock{ 38 | Hash: event.BlockHash, 39 | Number: event.BlockNumber, 40 | Timestamp: event.BlockTimestamp, 41 | }, 42 | AddressFrom: event.AddressFrom, 43 | AddressTo: event.AddressTo, 44 | NodeID: event.NodeID.Uint64(), 45 | Type: event.Type, 46 | LogIndex: event.LogIndex, 47 | ChainID: event.ChainID, 48 | Metadata: event.Metadata, 49 | Finalized: event.Finalized, 50 | } 51 | } 52 | 53 | func NewNodeEvents(events []*schema.NodeEvent) NodeEventsResponseData { 54 | result := make([]*NodeEvent, len(events)) 55 | for i, event := range events { 56 | result[i] = NewNodeEvent(event) 57 | } 58 | 59 | return result 60 | } 61 | -------------------------------------------------------------------------------- /internal/service/scheduler/enforcer/federated_handles/federated_handles.go: -------------------------------------------------------------------------------- 1 | package federatedhandles 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9" 12 | "github.com/rss3-network/global-indexer/common/httputil" 13 | "github.com/rss3-network/global-indexer/internal/cache" 14 | "github.com/rss3-network/global-indexer/internal/cronjob" 15 | "github.com/rss3-network/global-indexer/internal/database" 16 | "github.com/rss3-network/global-indexer/internal/service" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | var _ service.Server = (*server)(nil) 21 | 22 | var Name = "federated_handles" 23 | 24 | type server struct { 25 | cronJob *cronjob.CronJob 26 | databaseClient database.Client 27 | cacheClient cache.Client 28 | httpClient httputil.Client 29 | } 30 | 31 | func (s *server) Name() string { 32 | return Name 33 | } 34 | 35 | func (s *server) Spec() string { 36 | return "@every 15m" 37 | } 38 | 39 | func (s *server) Run(ctx context.Context) error { 40 | // initial execution of maintaining federated handles 41 | if err := s.maintainFederatedHandles(ctx); err != nil { 42 | zap.L().Error("initial execution of maintaining federated handles failed", zap.Error(err)) 43 | } 44 | 45 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 46 | if err := s.maintainFederatedHandles(ctx); err != nil { 47 | zap.L().Error("maintain federated handles error", zap.Error(err)) 48 | return 49 | } 50 | }) 51 | 52 | if err != nil { 53 | return fmt.Errorf("add maintain federated handles cron job: %w", err) 54 | } 55 | 56 | s.cronJob.Start() 57 | defer s.cronJob.Stop() 58 | 59 | stopChan := make(chan os.Signal, 1) 60 | 61 | signal.Notify(stopChan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 62 | <-stopChan 63 | 64 | return nil 65 | } 66 | 67 | func New(redisClient *redis.Client, databaseClient database.Client, httpClient httputil.Client) service.Server { 68 | return &server{ 69 | cronJob: cronjob.New(redisClient, Name, 10*time.Second), 70 | databaseClient: databaseClient, 71 | cacheClient: cache.New(redisClient), 72 | httpClient: httpClient, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/snapshot_staker.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 9 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 10 | "github.com/rss3-network/global-indexer/schema" 11 | "github.com/samber/lo" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func (n *NTA) GetStakerCountSnapshots(c echo.Context) error { 16 | stakeSnapshots, err := n.databaseClient.FindStakerCountSnapshots(c.Request().Context()) 17 | if err != nil { 18 | zap.L().Error("find staker_count snapshots", zap.Error(err)) 19 | 20 | return c.NoContent(http.StatusInternalServerError) 21 | } 22 | 23 | var response nta.Response 24 | 25 | response.Data = nta.NewStakerCountSnapshots(stakeSnapshots) 26 | 27 | return c.JSON(http.StatusOK, response) 28 | } 29 | 30 | func (n *NTA) GetStakerProfitSnapshots(c echo.Context) error { 31 | var request nta.GetStakerProfitSnapshotsRequest 32 | 33 | if err := c.Bind(&request); err != nil { 34 | return errorx.BadParamsError(c, fmt.Errorf("bind request: %w", err)) 35 | } 36 | 37 | if err := c.Validate(&request); err != nil { 38 | return errorx.ValidationFailedError(c, fmt.Errorf("validate failed: %w", err)) 39 | } 40 | 41 | query := schema.StakerProfitSnapshotsQuery{ 42 | OwnerAddress: lo.ToPtr(request.StakerAddress), 43 | Limit: request.Limit, 44 | Cursor: request.Cursor, 45 | BeforeDate: request.BeforeDate, 46 | AfterDate: request.AfterDate, 47 | } 48 | 49 | stakerProfitSnapshots, err := n.databaseClient.FindStakerProfitSnapshots(c.Request().Context(), query) 50 | if err != nil { 51 | zap.L().Error("find staker profit snapshots", zap.Error(err)) 52 | 53 | return c.NoContent(http.StatusInternalServerError) 54 | } 55 | 56 | var cursor string 57 | 58 | if request.Limit != nil && len(stakerProfitSnapshots) > 0 && len(stakerProfitSnapshots) == lo.FromPtr(request.Limit) { 59 | last, _ := lo.Last(stakerProfitSnapshots) 60 | cursor = fmt.Sprintf("%d", last.ID) 61 | } 62 | 63 | return c.JSON(http.StatusOK, nta.Response{ 64 | Data: stakerProfitSnapshots, 65 | Cursor: cursor, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /schema/bridge_transaction.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | type BridgeTransactionType string 11 | 12 | const ( 13 | BridgeTransactionTypeDeposit BridgeTransactionType = "deposit" 14 | BridgeTransactionTypeWithdraw BridgeTransactionType = "withdraw" 15 | ) 16 | 17 | type BridgeTransactionImporter interface { 18 | Import(bridgeTransaction BridgeTransaction) error 19 | } 20 | 21 | type BridgeTransactionExporter interface { 22 | Export() (*BridgeTransaction, error) 23 | } 24 | 25 | type BridgeTransactionTransformer interface { 26 | BridgeTransactionImporter 27 | BridgeTransactionExporter 28 | } 29 | 30 | type BridgeTransaction struct { 31 | ID common.Hash `json:"id"` 32 | Type BridgeTransactionType `json:"type"` 33 | Sender common.Address `json:"sender"` 34 | Receiver common.Address `json:"receiver"` 35 | TokenAddressL1 *common.Address `json:"token_address_l1"` 36 | TokenAddressL2 *common.Address `json:"token_address_l2"` 37 | TokenValue *big.Int `json:"token_value"` 38 | Data string `json:"data"` 39 | ChainID uint64 `json:"chain_id"` 40 | BlockTimestamp time.Time `json:"block_timestamp"` 41 | BlockNumber uint64 `json:"block_number"` 42 | TransactionIndex uint `json:"transaction_index"` 43 | Finalized bool `json:"finalized"` 44 | } 45 | 46 | type BridgeTransactionQuery struct { 47 | ID *common.Hash `query:"id"` 48 | Sender *common.Address `query:"sender"` 49 | Receiver *common.Address `query:"receiver"` 50 | Address *common.Address `query:"address"` 51 | Type *BridgeTransactionType `query:"type"` 52 | } 53 | 54 | type BridgeTransactionsQuery struct { 55 | Cursor *common.Hash `query:"cursor"` 56 | ID *common.Hash `query:"id"` 57 | Sender *common.Address `query:"sender"` 58 | Receiver *common.Address `query:"receiver"` 59 | Address *common.Address `query:"address"` 60 | Type *BridgeTransactionType `query:"type"` 61 | } 62 | -------------------------------------------------------------------------------- /schema/epoch.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/shopspring/decimal" 8 | ) 9 | 10 | // Epoch records an Epoch and its proof of rewards distribution 11 | type Epoch struct { 12 | ID uint64 `json:"id"` 13 | // StartTimestamp when an Epoch begins. 14 | StartTimestamp int64 `json:"start_timestamp"` 15 | // EndTimestamp when an Epoch ends. 16 | EndTimestamp int64 `json:"end_timestamp"` 17 | TransactionHash common.Hash `json:"transaction_hash"` 18 | TransactionIndex uint `json:"transaction_index"` 19 | BlockHash common.Hash `json:"block_hash"` 20 | BlockNumber *big.Int `json:"block_number"` 21 | BlockTimestamp int64 `json:"block_timestamp"` 22 | // total Operation Rewards distributed. 23 | TotalOperationRewards decimal.Decimal `json:"total_operation_rewards"` 24 | // total Staking Rewards distributed. 25 | TotalStakingRewards decimal.Decimal `json:"total_staking_rewards"` 26 | // the number of Nodes that received rewards. 27 | TotalRewardedNodes int `json:"total_rewarded_nodes"` 28 | // the list of Nodes that received rewards and the amount they received. 29 | RewardedNodes []*RewardedNode `json:"rewarded_nodes,omitempty"` 30 | // the total number of DSL requests made during the Epoch. 31 | TotalRequestCounts decimal.Decimal `json:"total_request_counts"` 32 | Finalized bool `json:"-"` 33 | CreatedAt int64 `json:"-"` 34 | UpdatedAt int64 `json:"-"` 35 | } 36 | 37 | type RewardedNode struct { 38 | EpochID uint64 `json:"epoch_id"` 39 | Index int `json:"index"` 40 | TransactionHash common.Hash `json:"transaction_hash"` 41 | NodeAddress common.Address `json:"node_address"` 42 | OperationRewards decimal.Decimal `json:"operation_rewards"` 43 | StakingRewards decimal.Decimal `json:"staking_rewards"` 44 | TaxCollected decimal.Decimal `json:"tax_collected"` 45 | RequestCount decimal.Decimal `json:"request_count"` 46 | } 47 | 48 | type FindEpochsQuery struct { 49 | EpochID *uint64 50 | Distinct *bool 51 | Limit *int 52 | Cursor *string 53 | BlockNumber *uint64 54 | Finalized *bool 55 | } 56 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/stake_chip.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | "net/url" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/rss3-network/global-indexer/contract/l2" 11 | "github.com/rss3-network/global-indexer/schema" 12 | "github.com/samber/lo" 13 | "github.com/shopspring/decimal" 14 | ) 15 | 16 | type GetStakeChipsRequest struct { 17 | Cursor *big.Int `query:"cursor"` 18 | IDs []*big.Int `query:"id"` 19 | Node *common.Address `query:"node"` 20 | Owner *common.Address `query:"owner"` 21 | Limit int `query:"limit" default:"50" min:"1" max:"200"` 22 | } 23 | 24 | type GetStakeChipRequest struct { 25 | ChipID *big.Int `param:"chip_id"` 26 | } 27 | 28 | type GetStakeChipsImageRequest struct { 29 | ChipID *big.Int `param:"chip_id"` 30 | } 31 | 32 | type GetStakeChipsResponseData []*StakeChip 33 | 34 | type GetStakeChipResponseData *StakeChip 35 | 36 | type StakeChip struct { 37 | ID *big.Int `json:"id"` 38 | Node common.Address `json:"node"` 39 | Owner common.Address `json:"owner"` 40 | Metadata json.RawMessage `json:"metadata"` 41 | Value decimal.Decimal `json:"value"` 42 | LatestValue decimal.Decimal `json:"latest_value"` 43 | Finalized bool `json:"finalized"` 44 | } 45 | 46 | func NewStakeChip(stakeChip *schema.StakeChip, baseURL url.URL) GetStakeChipResponseData { 47 | result := StakeChip{ 48 | ID: stakeChip.ID, 49 | Node: stakeChip.Node, 50 | Owner: stakeChip.Owner, 51 | Metadata: stakeChip.Metadata, 52 | Value: stakeChip.Value, 53 | LatestValue: stakeChip.LatestValue, 54 | Finalized: stakeChip.Finalized, 55 | } 56 | 57 | var tokenMetadata l2.ChipsTokenMetadata 58 | _ = json.Unmarshal(stakeChip.Metadata, &tokenMetadata) 59 | 60 | tokenMetadata.Image = baseURL.JoinPath(fmt.Sprintf("/nta/chips/%d/image.svg", result.ID)).String() 61 | 62 | result.Metadata, _ = json.Marshal(tokenMetadata) 63 | 64 | return &result 65 | } 66 | 67 | func NewStakeChips(stakeChips []*schema.StakeChip, baseURL url.URL) GetStakeChipsResponseData { 68 | return lo.Map(stakeChips, func(stakeChip *schema.StakeChip, _ int) *StakeChip { 69 | return NewStakeChip(stakeChip, baseURL) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /internal/cronjob/cronjob.go: -------------------------------------------------------------------------------- 1 | package cronjob 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-redsync/redsync/v4" 9 | "github.com/go-redsync/redsync/v4/redis/goredis/v9" 10 | "github.com/redis/go-redis/v9" 11 | "github.com/robfig/cron/v3" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type CronJob struct { 16 | crontab *cron.Cron 17 | mutex *redsync.Mutex 18 | timeout time.Duration 19 | } 20 | 21 | var KeyPrefix = "cronjob:%s" 22 | 23 | func (c *CronJob) AddFunc(ctx context.Context, spec string, cmd func()) error { 24 | _, err := c.crontab.AddFunc(spec, func() { 25 | ctx, cancel := context.WithCancel(ctx) 26 | defer cancel() 27 | 28 | if err := c.mutex.Lock(); err != nil { 29 | zap.L().Error("lock error", zap.String("key", c.mutex.Name()), zap.Error(err)) 30 | 31 | return 32 | } 33 | 34 | defer func() { 35 | if _, err := c.mutex.Unlock(); err != nil { 36 | zap.L().Error("release lock error", zap.String("key", c.mutex.Name()), zap.Error(err)) 37 | } 38 | }() 39 | 40 | c.Renewal(ctx) 41 | cmd() 42 | }) 43 | 44 | return err 45 | } 46 | 47 | func (c *CronJob) Renewal(ctx context.Context) { 48 | go func(ctx context.Context) { 49 | // Renewal lock every half of timeout. 50 | ticker := time.NewTicker(c.timeout / 2) 51 | defer ticker.Stop() 52 | 53 | for { 54 | select { 55 | case <-ctx.Done(): 56 | return 57 | case <-ticker.C: 58 | result, err := c.mutex.ExtendContext(ctx) 59 | if err != nil { 60 | zap.L().Error("extend lock error", zap.String("key", c.mutex.Name()), zap.Error(err)) 61 | 62 | continue 63 | } 64 | 65 | if !result { 66 | zap.L().Error("extend lock failed", zap.String("key", c.mutex.Name())) 67 | 68 | continue 69 | } 70 | } 71 | } 72 | }(ctx) 73 | } 74 | 75 | func (c *CronJob) Start() { 76 | c.crontab.Start() 77 | } 78 | 79 | func (c *CronJob) Stop() { 80 | c.crontab.Stop() 81 | } 82 | 83 | func New(redisClient *redis.Client, name string, timeout time.Duration) *CronJob { 84 | pool := goredis.NewPool(redisClient) 85 | rs := redsync.New(pool) 86 | 87 | return &CronJob{ 88 | crontab: cron.New(cron.WithLocation(time.UTC), cron.WithSeconds()), 89 | mutex: rs.NewMutex(fmt.Sprintf(KeyPrefix, name), redsync.WithExpiry(timeout)), 90 | timeout: timeout, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /schema/bridge_event.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/core/types" 9 | ) 10 | 11 | type BridgeEventType string 12 | 13 | const ( 14 | BridgeEventTypeDepositInitialized BridgeEventType = "initialized" 15 | BridgeEventTypeDepositFinalized BridgeEventType = "finalized" 16 | 17 | BridgeEventTypeWithdrawalInitialized BridgeEventType = "initialized" 18 | BridgeEventTypeWithdrawalProved BridgeEventType = "proved" 19 | BridgeEventTypeWithdrawalFinalized BridgeEventType = "finalized" 20 | ) 21 | 22 | type BridgeEventImporter interface { 23 | Import(bridgeEvent BridgeEvent) error 24 | } 25 | 26 | type BridgeEventExporter interface { 27 | Export() (*BridgeEvent, error) 28 | } 29 | 30 | type BridgeEventTransformer interface { 31 | BridgeEventImporter 32 | BridgeEventExporter 33 | } 34 | 35 | type BridgeEvent struct { 36 | ID common.Hash `json:"id"` 37 | Type BridgeEventType `json:"type"` 38 | TransactionHash common.Hash `json:"transaction_hash"` 39 | TransactionIndex uint `json:"transaction_index"` 40 | TransactionStatus uint64 `json:"transaction_status"` 41 | ChainID uint64 `json:"chain_id"` 42 | BlockHash common.Hash `json:"block_hash"` 43 | BlockNumber *big.Int `json:"block_number"` 44 | BlockTimestamp time.Time `json:"block_timestamp"` 45 | Finalized bool `json:"finalized"` 46 | } 47 | 48 | func NewBridgeEvent(id common.Hash, eventType BridgeEventType, chainID uint64, header *types.Header, transaction *types.Transaction, receipt *types.Receipt, finalized bool) *BridgeEvent { 49 | bridgeEvent := BridgeEvent{ 50 | ID: id, 51 | Type: eventType, 52 | TransactionHash: transaction.Hash(), 53 | TransactionIndex: receipt.TransactionIndex, 54 | TransactionStatus: receipt.Status, 55 | ChainID: chainID, 56 | BlockHash: header.Hash(), 57 | BlockNumber: header.Number, 58 | BlockTimestamp: time.Unix(int64(header.Time), 0), 59 | Finalized: finalized, 60 | } 61 | 62 | return &bridgeEvent 63 | } 64 | 65 | type BridgeEventsQuery struct { 66 | IDs []common.Hash `query:"ids"` 67 | } 68 | -------------------------------------------------------------------------------- /internal/service/hub/model/errorx/error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | //go:generate go run --mod=mod github.com/dmarkham/enumer --type=ErrorCode --transform=snake --values --trimprefix=ErrorCode --json --output error_code.go 12 | type ErrorCode int 13 | 14 | const ( 15 | ErrorCodeBadRequest ErrorCode = iota + 1 16 | ErrorCodeValidationFailed 17 | ErrorCodeBadParams 18 | ErrorCodeInternalError 19 | ErrorCodeServiceUnavailable 20 | ) 21 | 22 | var ErrNoNodesAvailable = errors.New("no Nodes are available to process this request") 23 | 24 | type ErrorResponse struct { 25 | Error string `json:"error"` 26 | ErrorCode ErrorCode `json:"error_code"` 27 | Details string `json:"details,omitempty"` 28 | } 29 | 30 | func BadRequestError(c echo.Context, err error) error { 31 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 32 | ErrorCode: ErrorCodeBadRequest, 33 | Error: "Invalid request. Please check your input and try again.", 34 | Details: fmt.Sprintf("%v", err), 35 | }) 36 | } 37 | 38 | func ValidationFailedError(c echo.Context, err error) error { 39 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 40 | ErrorCode: ErrorCodeValidationFailed, 41 | Error: "Validation failed. Ensure all fields meet the required criteria and try again.", 42 | Details: fmt.Sprintf("%v", err), 43 | }) 44 | } 45 | 46 | func BadParamsError(c echo.Context, err error) error { 47 | return c.JSON(http.StatusBadRequest, &ErrorResponse{ 48 | ErrorCode: ErrorCodeBadParams, 49 | Error: "Invalid parameter combination. Verify the combination and try again.", 50 | Details: fmt.Sprintf("%v", err), 51 | }) 52 | } 53 | 54 | func ServiceUnavailableError(c echo.Context, err error) error { 55 | return c.JSON(http.StatusServiceUnavailable, &ErrorResponse{ 56 | ErrorCode: ErrorCodeServiceUnavailable, 57 | Error: "The requested service is temporarily unavailable, please try again later.", 58 | Details: fmt.Sprintf("%v", err), 59 | }) 60 | } 61 | 62 | func InternalError(c echo.Context) error { 63 | return c.JSON(http.StatusInternalServerError, &ErrorResponse{ 64 | ErrorCode: ErrorCodeInternalError, 65 | Error: "An internal error has occurred, please try again later.", 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/service/hub/handler/dsl/metrics.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | "github.com/rss3-network/global-indexer/internal/service/hub/handler/dsl/model" 7 | ) 8 | 9 | var ( 10 | requestCounter = promauto.NewCounterVec( 11 | prometheus.CounterOpts{ 12 | Name: "dsl_get_activity_requests_total", 13 | Help: "Total number of GetActivity requests", 14 | }, 15 | []string{"endpoint"}, 16 | ) 17 | requestCounterByNetwork = promauto.NewCounterVec( 18 | prometheus.CounterOpts{ 19 | Name: "dsl_get_activity_requests_by_network_total", 20 | Help: "Total number of GetActivity requests by network", 21 | }, 22 | []string{"endpoint", "network"}, 23 | ) 24 | requestCounterByTag = promauto.NewCounterVec( 25 | prometheus.CounterOpts{ 26 | Name: "dsl_get_activity_requests_by_tag_total", 27 | Help: "Total number of GetActivity requests by tag", 28 | }, 29 | []string{"endpoint", "tag"}, 30 | ) 31 | requestCounterByPlatform = promauto.NewCounterVec( 32 | prometheus.CounterOpts{ 33 | Name: "dsl_get_activity_requests_by_platform_total", 34 | Help: "Total number of GetActivity requests by platform", 35 | }, 36 | []string{"endpoint", "platform"}, 37 | ) 38 | ) 39 | 40 | func incrementRequestCounter(endpoint string, networks []string, tags []string, platforms []string) { 41 | requestCounter.WithLabelValues(endpoint).Inc() 42 | 43 | if len(networks) > 0 { 44 | for _, t := range networks { 45 | requestCounterByNetwork.WithLabelValues(endpoint, t).Inc() 46 | } 47 | } else { 48 | for item := range model.NetworkToWorkersMap { 49 | requestCounterByNetwork.WithLabelValues(endpoint, item).Inc() 50 | } 51 | } 52 | 53 | if len(tags) > 0 { 54 | for _, t := range tags { 55 | requestCounterByTag.WithLabelValues(endpoint, t).Inc() 56 | } 57 | } else { 58 | for item := range model.TagToWorkersMap { 59 | requestCounterByTag.WithLabelValues(endpoint, item).Inc() 60 | } 61 | } 62 | 63 | if len(platforms) > 0 { 64 | for _, t := range platforms { 65 | requestCounterByPlatform.WithLabelValues(endpoint, t).Inc() 66 | } 67 | } else { 68 | for item := range model.PlatformToWorkersMap { 69 | requestCounterByPlatform.WithLabelValues(endpoint, item).Inc() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/average_tax_rate_submission.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | type AverageTaxRateSubmission struct { 12 | ID uint64 `gorm:"id"` 13 | EpochID uint64 `gorm:"epoch_id"` 14 | AverageTaxRate decimal.Decimal `gorm:"average_tax_rate"` 15 | TransactionHash string `gorm:"transaction_hash"` 16 | CreatedAt time.Time `gorm:"created_at"` 17 | UpdatedAt time.Time `gorm:"updated_at"` 18 | } 19 | 20 | func (a *AverageTaxRateSubmission) TableName() string { 21 | return "average_tax_rate_submissions" 22 | } 23 | 24 | func (a *AverageTaxRateSubmission) Import(submission *schema.AverageTaxRateSubmission) error { 25 | a.EpochID = submission.EpochID 26 | a.AverageTaxRate = submission.AverageTaxRate 27 | a.CreatedAt = submission.CreatedAt 28 | a.UpdatedAt = submission.UpdatedAt 29 | a.TransactionHash = submission.TransactionHash.String() 30 | 31 | return nil 32 | } 33 | 34 | func (a *AverageTaxRateSubmission) Export() (*schema.AverageTaxRateSubmission, error) { 35 | return &schema.AverageTaxRateSubmission{ 36 | ID: a.ID, 37 | EpochID: a.EpochID, 38 | AverageTaxRate: a.AverageTaxRate, 39 | TransactionHash: common.HexToHash(a.TransactionHash), 40 | CreatedAt: a.CreatedAt, 41 | UpdatedAt: a.UpdatedAt, 42 | }, nil 43 | } 44 | 45 | type AverageTaxSubmissions []AverageTaxRateSubmission 46 | 47 | func (a *AverageTaxSubmissions) Import(submissions []*schema.AverageTaxRateSubmission) error { 48 | for _, submission := range submissions { 49 | var imported AverageTaxRateSubmission 50 | 51 | if err := imported.Import(submission); err != nil { 52 | return err 53 | } 54 | 55 | *a = append(*a, imported) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (a *AverageTaxSubmissions) Export() ([]*schema.AverageTaxRateSubmission, error) { 62 | exported := make([]*schema.AverageTaxRateSubmission, 0) 63 | 64 | for _, submission := range *a { 65 | exportedSubmission, err := submission.Export() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | exported = append(exported, exportedSubmission) 71 | } 72 | 73 | return exported, nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/bridge_event.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | gorm "gorm.io/gorm/schema" 10 | ) 11 | 12 | var ( 13 | _ gorm.Tabler = (*BridgeEvent)(nil) 14 | _ schema.BridgeEventTransformer = (*BridgeEvent)(nil) 15 | ) 16 | 17 | type BridgeEvent struct { 18 | ID string `gorm:"column:id"` 19 | Type string `gorm:"column:type"` 20 | TransactionHash string `gorm:"column:transaction_hash;primaryKey"` 21 | TransactionIndex uint `gorm:"column:transaction_index"` 22 | TransactionStatus uint64 `gorm:"column:transaction_status"` 23 | ChainID uint64 `gorm:"column:chain_id"` 24 | BlockHash string `gorm:"column:block_hash;primaryKey"` 25 | BlockNumber uint64 `gorm:"column:block_number"` 26 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 27 | Finalized bool `gorm:"column:finalized"` 28 | } 29 | 30 | func (b *BridgeEvent) TableName() string { 31 | return "bridge.events" 32 | } 33 | 34 | func (b *BridgeEvent) Import(bridgeEvent schema.BridgeEvent) error { 35 | b.ID = bridgeEvent.ID.String() 36 | b.Type = string(bridgeEvent.Type) 37 | b.TransactionHash = bridgeEvent.TransactionHash.String() 38 | b.TransactionIndex = bridgeEvent.TransactionIndex 39 | b.TransactionStatus = bridgeEvent.TransactionStatus 40 | b.BlockHash = bridgeEvent.BlockHash.String() 41 | b.BlockNumber = bridgeEvent.BlockNumber.Uint64() 42 | b.BlockTimestamp = bridgeEvent.BlockTimestamp 43 | b.Finalized = bridgeEvent.Finalized 44 | 45 | return nil 46 | } 47 | 48 | func (b *BridgeEvent) Export() (*schema.BridgeEvent, error) { 49 | bridgeEvent := schema.BridgeEvent{ 50 | ID: common.HexToHash(b.ID), 51 | Type: schema.BridgeEventType(b.Type), 52 | TransactionHash: common.HexToHash(b.TransactionHash), 53 | TransactionIndex: b.TransactionIndex, 54 | TransactionStatus: b.TransactionStatus, 55 | ChainID: b.ChainID, 56 | BlockHash: common.HexToHash(b.BlockHash), 57 | BlockNumber: new(big.Int).SetUint64(b.BlockNumber), 58 | BlockTimestamp: b.BlockTimestamp, 59 | Finalized: b.Finalized, 60 | } 61 | 62 | return &bridgeEvent, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/nta.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "net/url" 7 | 8 | "github.com/ethereum-optimism/optimism/op-bindings/bindings" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/labstack/echo/v4" 11 | "github.com/rss3-network/global-indexer/common/geolite2" 12 | "github.com/rss3-network/global-indexer/common/httputil" 13 | "github.com/rss3-network/global-indexer/contract/l2" 14 | "github.com/rss3-network/global-indexer/internal/cache" 15 | "github.com/rss3-network/global-indexer/internal/config" 16 | "github.com/rss3-network/global-indexer/internal/database" 17 | ) 18 | 19 | type NTA struct { 20 | databaseClient database.Client 21 | stakingContract *l2.StakingV2MulticallClient 22 | networkParamsContract *l2.NetworkParams 23 | contractGovernanceToken *bindings.GovernanceToken 24 | geoLite2 *geolite2.Client 25 | cacheClient cache.Client 26 | httpClient httputil.Client 27 | erc20TokenMap map[common.Address]*bindings.GovernanceToken 28 | configFile *config.File 29 | chainL1ID uint64 30 | chainL2ID uint64 31 | } 32 | 33 | var MinDeposit = new(big.Int).Mul(big.NewInt(10000), big.NewInt(1e18)) 34 | 35 | func (n *NTA) baseURL(c echo.Context) url.URL { 36 | return url.URL{ 37 | Scheme: c.Scheme(), 38 | Host: c.Request().Host, 39 | } 40 | } 41 | 42 | func NewNTA(_ context.Context, configFile *config.File, databaseClient database.Client, stakingContract *l2.StakingV2MulticallClient, networkParamsContract *l2.NetworkParams, contractGovernanceToken *bindings.GovernanceToken, geoLite2 *geolite2.Client, cacheClient cache.Client, httpClient httputil.Client, erc20TokenMap map[common.Address]*bindings.GovernanceToken, chainL1ID, chainL2ID uint64) *NTA { 43 | return &NTA{ 44 | databaseClient: databaseClient, 45 | stakingContract: stakingContract, 46 | networkParamsContract: networkParamsContract, 47 | contractGovernanceToken: contractGovernanceToken, 48 | geoLite2: geoLite2, 49 | cacheClient: cacheClient, 50 | httpClient: httpClient, 51 | erc20TokenMap: erc20TokenMap, 52 | configFile: configFile, 53 | chainL1ID: chainL1ID, 54 | chainL2ID: chainL2ID, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /schema/node_event.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | ) 8 | 9 | type NodeEventType string 10 | 11 | const ( 12 | NodeEventNodeCreated NodeEventType = "nodeCreated" 13 | NodeEventNodeUpdated NodeEventType = "nodeUpdated" 14 | ) 15 | 16 | type NodeEvent struct { 17 | TransactionHash common.Hash `json:"transaction_hash"` 18 | TransactionIndex uint `json:"transaction_index"` 19 | NodeID *big.Int `json:"node_id"` 20 | AddressFrom common.Address `json:"address_from"` 21 | AddressTo common.Address `json:"address_to"` 22 | Type NodeEventType `json:"type"` 23 | LogIndex uint `json:"log_index"` 24 | ChainID uint64 `json:"chain_id"` 25 | BlockHash common.Hash `json:"block_hash"` 26 | BlockNumber *big.Int `json:"block_number"` 27 | BlockTimestamp int64 `json:"block_timestamp"` 28 | Metadata NodeEventMetadata `json:"metadata"` 29 | Finalized bool `json:"finalized"` 30 | } 31 | 32 | type NodeEventMetadata struct { 33 | NodeCreatedMetadata *NodeCreatedMetadata `json:"node_created,omitempty"` 34 | NodeUpdatedMetadata *NodeUpdatedMetadata `json:"node_updated,omitempty"` 35 | NodeUpdated2PublicGoodMetadata *NodeUpdated2PublicGoodMetadata `json:"node_updated_to_public_good,omitempty"` 36 | } 37 | 38 | type NodeCreatedMetadata struct { 39 | NodeID *big.Int `json:"node_id"` 40 | Address common.Address `json:"address"` 41 | Name string `json:"name"` 42 | Description string `json:"description"` 43 | TaxRateBasisPoints uint64 `json:"tax_rate_basis_points"` 44 | PublicGood bool `json:"public_good"` 45 | } 46 | 47 | type NodeUpdatedMetadata struct { 48 | Address common.Address `json:"address"` 49 | Name string `json:"name"` 50 | Description string `json:"description"` 51 | } 52 | 53 | type NodeUpdated2PublicGoodMetadata struct { 54 | Address common.Address `json:"address"` 55 | PublicGood bool `json:"public_good"` 56 | } 57 | 58 | type NodeEventsQuery struct { 59 | NodeAddress *common.Address 60 | Cursor *string 61 | Limit *int 62 | Finalized *bool 63 | Type *NodeEventType 64 | } 65 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/operator_profit_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | // FIXME: OperatorProfit -> NodeOperationProfit 12 | type OperatorProfitSnapshot struct { 13 | ID uint64 `gorm:"column:id"` 14 | Date time.Time `gorm:"column:date"` 15 | EpochID uint64 `gorm:"column:epoch_id"` 16 | Operator common.Address `gorm:"column:operator"` 17 | OperationPool decimal.Decimal `gorm:"column:operation_pool"` 18 | CreatedAt time.Time `gorm:"column:created_at"` 19 | UpdatedAt time.Time `gorm:"column:updated_at"` 20 | } 21 | 22 | func (s *OperatorProfitSnapshot) TableName() string { 23 | return "node.operator_profit_snapshots" 24 | } 25 | 26 | func (s *OperatorProfitSnapshot) Import(snapshot schema.OperatorProfitSnapshot) error { 27 | s.Date = snapshot.Date 28 | s.EpochID = snapshot.EpochID 29 | s.Operator = snapshot.Operator 30 | s.OperationPool = snapshot.OperationPool 31 | s.CreatedAt = snapshot.CreatedAt 32 | s.UpdatedAt = snapshot.UpdatedAt 33 | 34 | return nil 35 | } 36 | 37 | func (s *OperatorProfitSnapshot) Export() (*schema.OperatorProfitSnapshot, error) { 38 | return &schema.OperatorProfitSnapshot{ 39 | ID: s.ID, 40 | Date: s.Date, 41 | EpochID: s.EpochID, 42 | Operator: s.Operator, 43 | OperationPool: s.OperationPool, 44 | CreatedAt: s.CreatedAt, 45 | UpdatedAt: s.UpdatedAt, 46 | }, nil 47 | } 48 | 49 | type OperatorProfitSnapshots []OperatorProfitSnapshot 50 | 51 | func (s *OperatorProfitSnapshots) Import(snapshots []*schema.OperatorProfitSnapshot) error { 52 | for _, snapshot := range snapshots { 53 | var imported OperatorProfitSnapshot 54 | 55 | if err := imported.Import(*snapshot); err != nil { 56 | return err 57 | } 58 | 59 | *s = append(*s, imported) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (s *OperatorProfitSnapshots) Export() ([]*schema.OperatorProfitSnapshot, error) { 66 | snapshots := make([]*schema.OperatorProfitSnapshot, 0) 67 | 68 | for _, snapshot := range *s { 69 | exported, err := snapshot.Export() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | snapshots = append(snapshots, exported) 75 | } 76 | 77 | return snapshots, nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/service/hub/handler/nta/network.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "net/http" 8 | 9 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 10 | "github.com/labstack/echo/v4" 11 | "github.com/rss3-network/global-indexer/internal/service/hub/model/errorx" 12 | "github.com/rss3-network/global-indexer/internal/service/hub/model/nta" 13 | ) 14 | 15 | // GetAssets returns all assets supported by the DSL. 16 | func (n *NTA) GetAssets(c echo.Context) error { 17 | // Get parameters for the current epoch from networkParams 18 | params, err := n.networkParamsContract.GetParams(&bind.CallOpts{}, math.MaxUint64) 19 | 20 | if err != nil { 21 | return errorx.BadParamsError(c, fmt.Errorf("failed to get params for epoch %w", err)) 22 | } 23 | 24 | var networkParam nta.NetworkParamsData 25 | if err = json.Unmarshal([]byte(params), &networkParam); err != nil { 26 | return errorx.BadParamsError(c, fmt.Errorf("failed to unmarshal network params %w", err)) 27 | } 28 | 29 | return c.JSON(http.StatusOK, nta.Response{Data: struct { 30 | Networks map[string]nta.Asset `json:"networks"` 31 | Workers map[string]nta.Asset `json:"workers"` 32 | }{ 33 | Networks: networkParam.NetworkAssets, 34 | Workers: networkParam.WorkerAssets, 35 | }}) 36 | } 37 | 38 | // GetNetworkConfig returns the network configuration for the current epoch. 39 | func (n *NTA) GetNetworkConfig(c echo.Context) error { 40 | // Get parameters for the current epoch from networkParams 41 | params, err := n.networkParamsContract.GetParams(&bind.CallOpts{}, math.MaxUint64) 42 | 43 | if err != nil { 44 | return errorx.BadParamsError(c, fmt.Errorf("failed to get params for epoch %w", err)) 45 | } 46 | 47 | var networkParam nta.NetworkParamsData 48 | if err = json.Unmarshal([]byte(params), &networkParam); err != nil { 49 | return errorx.BadParamsError(c, fmt.Errorf("failed to unmarshal network params %w", err)) 50 | } 51 | 52 | return c.JSON(http.StatusOK, nta.Response{Data: struct { 53 | RSSConfig any `json:"rss"` 54 | DecentralizedConfig any `json:"decentralized"` 55 | FederatedConfig any `json:"federated"` 56 | AIConfig any `json:"ai"` 57 | }{ 58 | RSSConfig: networkParam.NetworkConfig["rss"], 59 | DecentralizedConfig: networkParam.NetworkConfig["decentralized"], 60 | FederatedConfig: networkParam.NetworkConfig["federated"], 61 | AIConfig: networkParam.NetworkConfig["ai"], 62 | }}) 63 | } 64 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/stake_event.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | "time" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/rss3-network/global-indexer/schema" 10 | gorm "gorm.io/gorm/schema" 11 | ) 12 | 13 | var ( 14 | _ gorm.Tabler = (*StakeEvent)(nil) 15 | _ schema.StakeEventTransformer = (*StakeEvent)(nil) 16 | ) 17 | 18 | type StakeEvent struct { 19 | ID string `gorm:"column:id"` 20 | Type string `gorm:"column:type"` 21 | TransactionHash string `gorm:"column:transaction_hash;primaryKey"` 22 | TransactionIndex uint `gorm:"column:transaction_index"` 23 | TransactionStatus uint64 `gorm:"column:transaction_status"` 24 | LogIndex uint `gorm:"column:log_index"` 25 | Metadata json.RawMessage `gorm:"column:metadata"` 26 | BlockHash string `gorm:"column:block_hash;primaryKey"` 27 | BlockNumber uint64 `gorm:"column:block_number"` 28 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 29 | Finalized bool `gorm:"column:finalized"` 30 | } 31 | 32 | func (b *StakeEvent) TableName() string { 33 | return "stake.events" 34 | } 35 | 36 | func (b *StakeEvent) Import(stakeEvent schema.StakeEvent) error { 37 | b.ID = stakeEvent.ID.String() 38 | b.Type = string(stakeEvent.Type) 39 | b.TransactionHash = stakeEvent.TransactionHash.String() 40 | b.TransactionIndex = stakeEvent.TransactionIndex 41 | b.TransactionStatus = stakeEvent.TransactionStatus 42 | b.LogIndex = stakeEvent.LogIndex 43 | b.Metadata = stakeEvent.Metadata 44 | b.BlockHash = stakeEvent.BlockHash.String() 45 | b.BlockNumber = stakeEvent.BlockNumber.Uint64() 46 | b.BlockTimestamp = stakeEvent.BlockTimestamp 47 | b.Finalized = stakeEvent.Finalized 48 | 49 | return nil 50 | } 51 | 52 | func (b *StakeEvent) Export() (*schema.StakeEvent, error) { 53 | stakeEvent := schema.StakeEvent{ 54 | ID: common.HexToHash(b.ID), 55 | Type: schema.StakeEventType(b.Type), 56 | TransactionHash: common.HexToHash(b.TransactionHash), 57 | TransactionIndex: b.TransactionIndex, 58 | TransactionStatus: b.TransactionStatus, 59 | LogIndex: b.LogIndex, 60 | Metadata: b.Metadata, 61 | BlockHash: common.HexToHash(b.BlockHash), 62 | BlockNumber: new(big.Int).SetUint64(b.BlockNumber), 63 | BlockTimestamp: b.BlockTimestamp, 64 | Finalized: b.Finalized, 65 | } 66 | 67 | return &stakeEvent, nil 68 | } 69 | -------------------------------------------------------------------------------- /contract/l2/multicall_test.go: -------------------------------------------------------------------------------- 1 | package l2_test 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/ethclient" 10 | "github.com/rss3-network/global-indexer/contract/l2" 11 | "github.com/rss3-network/global-indexer/contract/multicall3" 12 | "github.com/samber/lo" 13 | "github.com/shopspring/decimal" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestStakingV2GetChipsInfoByMulticall(t *testing.T) { 18 | t.Parallel() 19 | 20 | type arguments struct { 21 | chainID uint64 22 | blockNumber *big.Int 23 | ChipIDs []*big.Int 24 | } 25 | 26 | testcases := []struct { 27 | name string 28 | arguments arguments 29 | want []l2.ChipInfo 30 | }{ 31 | { 32 | name: "Get Mainnet Chips Info", 33 | arguments: arguments{ 34 | chainID: multicall3.ChainIDRSS3Mainnet, 35 | blockNumber: big.NewInt(6023346), 36 | ChipIDs: []*big.Int{ 37 | big.NewInt(1869), 38 | big.NewInt(1870), 39 | big.NewInt(1671), 40 | }, 41 | }, 42 | want: []l2.ChipInfo{ 43 | { 44 | NodeAddr: common.HexToAddress("0x39f9e912c1f696f533e7a2267ea233aec9742b35"), 45 | Tokens: lo.Must(decimal.NewFromString("677912168357482297897")).BigInt(), 46 | Shares: lo.Must(decimal.NewFromString("500000000000000000000")).BigInt(), 47 | }, 48 | { 49 | NodeAddr: common.HexToAddress("0x3Ca85BD1eB958C0aBC9D06684C5Ac01f71029DD5"), 50 | Tokens: lo.Must(decimal.NewFromString("648814753997254162748")).BigInt(), 51 | Shares: lo.Must(decimal.NewFromString("500000000000000000000")).BigInt(), 52 | }, 53 | { 54 | NodeAddr: common.HexToAddress("0xc8b960d09c0078c18dcbe7eb9ab9d816bcca8944"), 55 | Tokens: lo.Must(decimal.NewFromString("639898391318730922465")).BigInt(), 56 | Shares: lo.Must(decimal.NewFromString("500000000000000000000")).BigInt(), 57 | }, 58 | }, 59 | }, 60 | } 61 | 62 | for _, testcase := range testcases { 63 | testcase := testcase 64 | 65 | ctx := context.Background() 66 | 67 | ethereumClient, err := ethclient.DialContext(ctx, "https://rpc.rss3.io") 68 | require.NoError(t, err) 69 | 70 | client, err := l2.NewStakingV2MulticallClient(multicall3.ChainIDRSS3Mainnet, ethereumClient) 71 | require.NoError(t, err) 72 | 73 | t.Run(testcase.name, func(t *testing.T) { 74 | t.Parallel() 75 | 76 | chipsInfo, err := client.StakingV2GetChipsInfo(ctx, testcase.arguments.blockNumber, testcase.arguments.ChipIDs) 77 | require.NoError(t, err) 78 | 79 | require.Equal(t, testcase.want, chipsInfo) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/staker_profit_snapshot.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/shopspring/decimal" 9 | ) 10 | 11 | type StakerProfitSnapshot struct { 12 | ID uint64 `gorm:"column:id"` 13 | Date time.Time `gorm:"column:date"` 14 | EpochID uint64 `gorm:"column:epoch_id"` 15 | OwnerAddress common.Address `gorm:"column:owner_address"` 16 | TotalChipAmount decimal.Decimal `gorm:"column:total_chip_amounts"` // Fixme: total_chip_amounts-> total_chip_amount 17 | TotalChipValue decimal.Decimal `gorm:"column:total_chip_values"` // Fixme: total_chip_values-> total_chip_value 18 | CreatedAt time.Time `gorm:"column:created_at"` 19 | UpdatedAt time.Time `gorm:"column:updated_at"` 20 | } 21 | 22 | func (s *StakerProfitSnapshot) TableName() string { 23 | return "stake.profit_snapshots" 24 | } 25 | 26 | func (s *StakerProfitSnapshot) Import(snapshot schema.StakerProfitSnapshot) error { 27 | s.Date = snapshot.Date 28 | s.EpochID = snapshot.EpochID 29 | s.OwnerAddress = snapshot.OwnerAddress 30 | s.TotalChipAmount = snapshot.TotalChipAmount 31 | s.TotalChipValue = snapshot.TotalChipValue 32 | s.CreatedAt = snapshot.CreatedAt 33 | s.UpdatedAt = snapshot.UpdatedAt 34 | 35 | return nil 36 | } 37 | 38 | func (s *StakerProfitSnapshot) Export() (*schema.StakerProfitSnapshot, error) { 39 | return &schema.StakerProfitSnapshot{ 40 | ID: s.ID, 41 | Date: s.Date, 42 | EpochID: s.EpochID, 43 | OwnerAddress: s.OwnerAddress, 44 | TotalChipAmount: s.TotalChipAmount, 45 | TotalChipValue: s.TotalChipValue, 46 | CreatedAt: s.CreatedAt, 47 | UpdatedAt: s.UpdatedAt, 48 | }, nil 49 | } 50 | 51 | type StakerProfitSnapshots []StakerProfitSnapshot 52 | 53 | func (s *StakerProfitSnapshots) Import(snapshots []*schema.StakerProfitSnapshot) error { 54 | for _, snapshot := range snapshots { 55 | var imported StakerProfitSnapshot 56 | 57 | if err := imported.Import(*snapshot); err != nil { 58 | return err 59 | } 60 | 61 | *s = append(*s, imported) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (s *StakerProfitSnapshots) Export() ([]*schema.StakerProfitSnapshot, error) { 68 | snapshots := make([]*schema.StakerProfitSnapshot, 0) 69 | 70 | for _, snapshot := range *s { 71 | exported, err := snapshot.Export() 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | snapshots = append(snapshots, exported) 77 | } 78 | 79 | return snapshots, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/service/scheduler/snapshot/server.go: -------------------------------------------------------------------------------- 1 | package snapshot 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ethereum/go-ethereum/ethclient" 8 | "github.com/redis/go-redis/v9" 9 | "github.com/rss3-network/global-indexer/contract/l2" 10 | stakingv2 "github.com/rss3-network/global-indexer/contract/l2/staking/v2" 11 | "github.com/rss3-network/global-indexer/internal/database" 12 | "github.com/rss3-network/global-indexer/internal/service" 13 | "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot/apy" 14 | nodecount "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot/node_count" 15 | operatorprofit "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot/operator_profit" 16 | stakercount "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot/staker_count" 17 | stakerprofit "github.com/rss3-network/global-indexer/internal/service/scheduler/snapshot/staker_profit" 18 | "github.com/sourcegraph/conc/pool" 19 | ) 20 | 21 | var Name = "snapshot" 22 | 23 | var _ service.Server = (*server)(nil) 24 | 25 | type server struct { 26 | snapshots []service.Server 27 | } 28 | 29 | func (s *server) Name() string { 30 | return Name 31 | } 32 | 33 | func (s *server) Run(ctx context.Context) error { 34 | errorPool := pool.New().WithContext(ctx).WithCancelOnError().WithFirstError() 35 | 36 | for _, snapshot := range s.snapshots { 37 | snapshot := snapshot 38 | 39 | errorPool.Go(func(ctx context.Context) error { 40 | return snapshot.Run(ctx) 41 | }) 42 | } 43 | 44 | return errorPool.Wait() 45 | } 46 | 47 | func New(databaseClient database.Client, redis *redis.Client, ethereumClient *ethclient.Client) (service.Server, error) { 48 | chainID, err := ethereumClient.ChainID(context.Background()) 49 | if err != nil { 50 | return nil, fmt.Errorf("get chain id: %w", err) 51 | } 52 | 53 | contractAddresses := l2.ContractMap[chainID.Uint64()] 54 | if contractAddresses == nil { 55 | return nil, fmt.Errorf("contract address not found for chain id: %d", chainID.Uint64()) 56 | } 57 | 58 | stakingContract, err := stakingv2.NewStaking(contractAddresses.AddressStakingProxy, ethereumClient) 59 | if err != nil { 60 | return nil, fmt.Errorf("new staking contract: %w", err) 61 | } 62 | 63 | return &server{ 64 | snapshots: []service.Server{ 65 | nodecount.New(databaseClient, redis), 66 | stakercount.New(databaseClient, redis), 67 | stakerprofit.New(databaseClient, redis, stakingContract), 68 | operatorprofit.New(databaseClient, redis, stakingContract), 69 | apy.New(databaseClient, redis, stakingContract), 70 | }, 71 | }, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/cache/client.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type Client interface { 12 | Get(ctx context.Context, key string, dest interface{}) error 13 | Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error 14 | IncrBy(ctx context.Context, key string, value int64) error 15 | PSubscribe(ctx context.Context, pattern string) *redis.PubSub 16 | ZAdd(ctx context.Context, key string, members ...redis.Z) error 17 | ZRem(ctx context.Context, key string, members ...interface{}) error 18 | ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) ([]redis.Z, error) 19 | Exists(ctx context.Context, key string) (int64, error) 20 | Pipeline(ctx context.Context) redis.Pipeliner 21 | } 22 | 23 | var _ Client = (*client)(nil) 24 | 25 | type client struct { 26 | redisClient *redis.Client 27 | } 28 | 29 | func (c *client) Get(ctx context.Context, key string, dest interface{}) error { 30 | data, err := c.redisClient.Get(ctx, key).Bytes() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return json.Unmarshal(data, dest) 36 | } 37 | 38 | func (c *client) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { 39 | data, err := json.Marshal(value) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return c.redisClient.Set(ctx, key, data, expiration).Err() 45 | } 46 | 47 | func (c *client) IncrBy(ctx context.Context, key string, value int64) error { 48 | return c.redisClient.IncrBy(ctx, key, value).Err() 49 | } 50 | 51 | func (c *client) PSubscribe(ctx context.Context, pattern string) *redis.PubSub { 52 | return c.redisClient.PSubscribe(ctx, pattern) 53 | } 54 | 55 | func (c *client) ZAdd(ctx context.Context, key string, members ...redis.Z) error { 56 | return c.redisClient.ZAdd(ctx, key, members...).Err() 57 | } 58 | 59 | func (c *client) ZRem(ctx context.Context, key string, members ...interface{}) error { 60 | return c.redisClient.ZRem(ctx, key, members...).Err() 61 | } 62 | 63 | func (c *client) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) ([]redis.Z, error) { 64 | return c.redisClient.ZRevRangeWithScores(ctx, key, start, stop).Result() 65 | } 66 | 67 | func (c *client) Exists(ctx context.Context, key string) (int64, error) { 68 | return c.redisClient.Exists(ctx, key).Result() 69 | } 70 | 71 | func (c *client) Pipeline(_ context.Context) redis.Pipeliner { 72 | return c.redisClient.Pipeline() 73 | } 74 | 75 | func New(redisClient *redis.Client) Client { 76 | return &client{ 77 | redisClient: redisClient, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /common/txmgr/config.go: -------------------------------------------------------------------------------- 1 | package txmgr 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Config houses parameters for altering the behavior of a SimpleTxManager. 9 | type Config struct { 10 | // ResubmissionTimeout is the interval at which, if no previously 11 | // published transaction has been mined, the new tx with a bumped gas 12 | // price will be published. Only one publication at MaxGasPrice will be 13 | // attempted. 14 | ResubmissionTimeout time.Duration 15 | 16 | // The multiplier applied to fee suggestions to put a hard limit on fee increases. 17 | FeeLimitMultiplier uint64 18 | 19 | // TxSendTimeout is how long to wait for sending a transaction. 20 | // By default it is unbounded. If set, this is recommended to be at least 20 minutes. 21 | TxSendTimeout time.Duration 22 | 23 | // TxNotInMempoolTimeout is how long to wait before aborting a transaction send if the transaction does not 24 | // make it to the mempool. If the tx is in the mempool, TxSendTimeout is used instead. 25 | TxNotInMempoolTimeout time.Duration 26 | 27 | // NetworkTimeout is the allowed duration for a single network request. 28 | // This is intended to be used for network requests that can be replayed. 29 | NetworkTimeout time.Duration 30 | 31 | // RequireQueryInterval is the interval at which the tx manager will 32 | // query the backend to check for confirmations after a tx at a 33 | // specific gas price has been published. 34 | ReceiptQueryInterval time.Duration 35 | 36 | // NumConfirmations specifies how many blocks are need to consider a 37 | // transaction confirmed. 38 | NumConfirmations uint64 39 | 40 | // SafeAbortNonceTooLowCount specifies how many ErrNonceTooLow observations 41 | // are required to give up on a tx at a particular nonce without receiving 42 | // confirmation. 43 | SafeAbortNonceTooLowCount uint64 44 | } 45 | 46 | func (m Config) Check() error { 47 | if m.NumConfirmations == 0 { 48 | return fmt.Errorf("NumConfirmations must not be 0") 49 | } 50 | 51 | if m.NetworkTimeout == 0 { 52 | return fmt.Errorf("must provide NetworkTimeout") 53 | } 54 | 55 | if m.FeeLimitMultiplier == 0 { 56 | return fmt.Errorf("must provide FeeLimitMultiplier") 57 | } 58 | 59 | if m.ResubmissionTimeout == 0 { 60 | return fmt.Errorf("must provide ResubmissionTimeout") 61 | } 62 | 63 | if m.ReceiptQueryInterval == 0 { 64 | return fmt.Errorf("must provide ReceiptQueryInterval") 65 | } 66 | 67 | if m.TxNotInMempoolTimeout == 0 { 68 | return fmt.Errorf("must provide TxNotInMempoolTimeout") 69 | } 70 | 71 | if m.SafeAbortNonceTooLowCount == 0 { 72 | return fmt.Errorf("SafeAbortNonceTooLowCount must not be 0") 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/stake_transaction.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/lib/pq" 9 | "github.com/rss3-network/global-indexer/schema" 10 | "github.com/samber/lo" 11 | "github.com/shopspring/decimal" 12 | gorm "gorm.io/gorm/schema" 13 | ) 14 | 15 | var ( 16 | _ gorm.Tabler = (*StakeTransaction)(nil) 17 | _ schema.StakeTransactionTransformer = (*StakeTransaction)(nil) 18 | ) 19 | 20 | type StakeTransaction struct { 21 | ID string `gorm:"column:id;primaryKey"` 22 | Type string `gorm:"column:type;primaryKey"` 23 | User string `gorm:"column:user"` 24 | Node string `gorm:"column:node"` 25 | Value decimal.Decimal `gorm:"column:value"` 26 | ChipIDs pq.Int64Array `gorm:"column:chips;type:bigint[]"` 27 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 28 | BlockNumber uint64 `gorm:"column:block_number"` 29 | TransactionIndex uint `gorm:"column:transaction_index"` 30 | Finalized bool `gorm:"column:finalized"` 31 | } 32 | 33 | func (s *StakeTransaction) TableName() string { 34 | return "stake.transactions" 35 | } 36 | 37 | func (s *StakeTransaction) Export() (*schema.StakeTransaction, error) { 38 | var stakeTransaction = schema.StakeTransaction{ 39 | ID: common.HexToHash(s.ID), 40 | Type: schema.StakeTransactionType(s.Type), 41 | User: common.HexToAddress(s.User), 42 | Node: common.HexToAddress(s.Node), 43 | Value: s.Value.BigInt(), 44 | ChipIDs: lo.Map(s.ChipIDs, func(value int64, _ int) *big.Int { 45 | return new(big.Int).SetInt64(value) 46 | }), 47 | BlockTimestamp: s.BlockTimestamp, 48 | BlockNumber: s.BlockNumber, 49 | TransactionIndex: s.TransactionIndex, 50 | Finalized: s.Finalized, 51 | } 52 | 53 | return &stakeTransaction, nil 54 | } 55 | 56 | func (s *StakeTransaction) Import(stakeTransaction schema.StakeTransaction) error { 57 | s.ID = stakeTransaction.ID.String() 58 | s.Type = string(stakeTransaction.Type) 59 | s.User = stakeTransaction.User.String() 60 | s.Node = stakeTransaction.Node.String() 61 | s.Value = decimal.NewFromBigInt(stakeTransaction.Value, 0) 62 | s.ChipIDs = lo.Map(stakeTransaction.ChipIDs, func(value *big.Int, _ int) int64 { 63 | return value.Int64() 64 | }) 65 | s.BlockTimestamp = stakeTransaction.BlockTimestamp 66 | s.BlockNumber = stakeTransaction.BlockNumber 67 | s.TransactionIndex = stakeTransaction.TransactionIndex 68 | s.Finalized = stakeTransaction.Finalized 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/service/hub/model/nta/stake_staking.go: -------------------------------------------------------------------------------- 1 | package nta 2 | 3 | import ( 4 | "net/url" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | "github.com/samber/lo" 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | type GetStakeStakingsRequest struct { 14 | Cursor *string `query:"cursor"` 15 | StakerAddress *common.Address `query:"staker_address"` 16 | NodeAddress *common.Address `query:"node_address"` 17 | Limit int `query:"limit" default:"50" min:"1" max:"100"` 18 | } 19 | 20 | type GetStakerProfitRequest struct { 21 | StakerAddress common.Address `param:"staker_address" validate:"required"` 22 | } 23 | 24 | type GetStakeStakingsResponseData []*StakeStaking 25 | 26 | type GetStakerProfitResponseData struct { 27 | Owner common.Address `json:"owner"` 28 | TotalChipAmount decimal.Decimal `json:"total_chip_amount"` 29 | TotalChipValue decimal.Decimal `json:"total_chip_value"` 30 | OneDay *GetStakerProfitChangesSinceResponseData `json:"one_day"` 31 | OneWeek *GetStakerProfitChangesSinceResponseData `json:"one_week"` 32 | OneMonth *GetStakerProfitChangesSinceResponseData `json:"one_month"` 33 | } 34 | 35 | type GetStakerProfitChangesSinceResponseData struct { 36 | Date time.Time `json:"date"` 37 | TotalChipAmount decimal.Decimal `json:"total_chip_amount"` 38 | TotalChipValue decimal.Decimal `json:"total_chip_value"` 39 | ProfitAndLoss decimal.Decimal `json:"profit_and_loss"` 40 | } 41 | 42 | type GetStakingStatRequest struct { 43 | Address common.Address `param:"staker_address" validate:"required"` 44 | } 45 | 46 | type StakeStaking struct { 47 | Staker common.Address `json:"staker,omitempty"` 48 | Node common.Address `json:"node,omitempty"` 49 | Value decimal.Decimal `json:"value"` 50 | Chips StakeStakingChips `json:"chips"` 51 | } 52 | 53 | type StakeStakingChips struct { 54 | Total uint64 `json:"total"` 55 | Showcase []*StakeChip `json:"showcase"` 56 | } 57 | 58 | func NewStakeAddress(stakeAddress *schema.StakeStaking, baseURL url.URL) *StakeStaking { 59 | return &StakeStaking{ 60 | Staker: stakeAddress.Staker, 61 | Node: stakeAddress.Node, 62 | Value: stakeAddress.Value, 63 | Chips: StakeStakingChips{ 64 | Total: stakeAddress.Chips.Total, 65 | Showcase: NewStakeChips(stakeAddress.Chips.Showcase, baseURL), 66 | }, 67 | } 68 | } 69 | 70 | func NewStakeStaking(stakeStakings []*schema.StakeStaking, baseURL url.URL) GetStakeStakingsResponseData { 71 | return lo.Map(stakeStakings, func(stakeStaking *schema.StakeStaking, _ int) *StakeStaking { 72 | return NewStakeAddress(stakeStaking, baseURL) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_reward_record.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/rss3-network/global-indexer/schema" 6 | "github.com/shopspring/decimal" 7 | ) 8 | 9 | // NodeRewardRecord stores rewards information for a Node in an Epoch 10 | type NodeRewardRecord struct { 11 | EpochID uint64 `gorm:"column:epoch_id;"` 12 | Index int `gorm:"column:index;primaryKey"` 13 | TransactionHash string `gorm:"column:transaction_hash;primaryKey"` 14 | NodeAddress string `gorm:"column:node_address"` 15 | OperationRewards decimal.Decimal `gorm:"column:operation_rewards"` 16 | StakingRewards decimal.Decimal `gorm:"column:staking_rewards"` 17 | TaxCollected decimal.Decimal `gorm:"column:tax_collected"` 18 | RequestCount decimal.Decimal `gorm:"column:request_count"` 19 | } 20 | 21 | func (e *NodeRewardRecord) TableName() string { 22 | return "node_reward_record" 23 | } 24 | 25 | func (e *NodeRewardRecord) Import(nodeToReward *schema.RewardedNode) error { 26 | e.EpochID = nodeToReward.EpochID 27 | e.Index = nodeToReward.Index 28 | e.TransactionHash = nodeToReward.TransactionHash.String() 29 | e.NodeAddress = nodeToReward.NodeAddress.String() 30 | e.OperationRewards = nodeToReward.OperationRewards 31 | e.StakingRewards = nodeToReward.StakingRewards 32 | e.TaxCollected = nodeToReward.TaxCollected 33 | e.RequestCount = nodeToReward.RequestCount 34 | 35 | return nil 36 | } 37 | 38 | func (e *NodeRewardRecord) Export() (*schema.RewardedNode, error) { 39 | return &schema.RewardedNode{ 40 | EpochID: e.EpochID, 41 | Index: e.Index, 42 | TransactionHash: common.HexToHash(e.TransactionHash), 43 | NodeAddress: common.HexToAddress(e.NodeAddress), 44 | OperationRewards: e.OperationRewards, 45 | StakingRewards: e.StakingRewards, 46 | TaxCollected: e.TaxCollected, 47 | RequestCount: e.RequestCount, 48 | }, nil 49 | } 50 | 51 | type EpochItems []*NodeRewardRecord 52 | 53 | func (e *EpochItems) Import(nodesToReward []*schema.RewardedNode) error { 54 | *e = make([]*NodeRewardRecord, 0, len(nodesToReward)) 55 | 56 | for index, nodeToReward := range nodesToReward { 57 | epochItem := &NodeRewardRecord{} 58 | if err := epochItem.Import(nodeToReward); err != nil { 59 | return err 60 | } 61 | 62 | epochItem.Index = index 63 | 64 | *e = append(*e, epochItem) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (e *EpochItems) Export() ([]*schema.RewardedNode, error) { 71 | items := make([]*schema.RewardedNode, 0, len(*e)) 72 | 73 | for _, epochItem := range *e { 74 | epochRewardItem, err := epochItem.Export() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | items = append(items, epochRewardItem) 80 | } 81 | 82 | return items, nil 83 | } 84 | -------------------------------------------------------------------------------- /common/crypto/signature.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "fmt" 8 | "math/big" 9 | "strings" 10 | 11 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/ethereum/go-ethereum/core/types" 14 | "github.com/ethereum/go-ethereum/crypto" 15 | gisigner "github.com/rss3-network/global-indexer/common/signer" 16 | ) 17 | 18 | func PrivateKeySignerFn(key *ecdsa.PrivateKey, chainID *big.Int) bind.SignerFn { 19 | from := crypto.PubkeyToAddress(key.PublicKey) 20 | signer := types.LatestSignerForChainID(chainID) 21 | 22 | return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { 23 | if address != from { 24 | return nil, bind.ErrNotAuthorized 25 | } 26 | 27 | signature, err := crypto.Sign(signer.Hash(tx).Bytes(), key) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return tx.WithSignature(signer, signature) 33 | } 34 | } 35 | 36 | type SignerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error) 37 | 38 | type SignerFactory func(chainID *big.Int) SignerFn 39 | 40 | func NewSignerFactory(privateKey, endpoint, address string) (SignerFactory, common.Address, error) { 41 | var ( 42 | signer SignerFactory 43 | fromAddress common.Address 44 | ) 45 | 46 | if endpoint != "" && address != "" { 47 | signerClient, err := gisigner.NewSignerClient(endpoint) 48 | if err != nil { 49 | return nil, common.Address{}, fmt.Errorf("failed to create the signer client: %w", err) 50 | } 51 | 52 | fromAddress = common.HexToAddress(address) 53 | signer = func(chainID *big.Int) SignerFn { 54 | return func(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { 55 | if !bytes.Equal(address[:], fromAddress[:]) { 56 | return nil, fmt.Errorf("attempting to sign for %s, expected %s: ", address, address) 57 | } 58 | 59 | return signerClient.SignTransaction(ctx, chainID, address, tx) 60 | } 61 | } 62 | } else { 63 | var ( 64 | privKey *ecdsa.PrivateKey 65 | 66 | err error 67 | ) 68 | 69 | if privateKey == "" { 70 | return nil, common.Address{}, fmt.Errorf("at least specify a private key") 71 | } 72 | 73 | privKey, err = crypto.HexToECDSA(strings.TrimPrefix(privateKey, "0x")) 74 | if err != nil { 75 | return nil, common.Address{}, fmt.Errorf("failed to parse the private key: %w", err) 76 | } 77 | 78 | fromAddress = crypto.PubkeyToAddress(privKey.PublicKey) 79 | signer = func(chainID *big.Int) SignerFn { 80 | s := PrivateKeySignerFn(privKey, chainID) 81 | 82 | return func(_ context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) { 83 | return s(addr, tx) 84 | } 85 | } 86 | } 87 | 88 | return signer, fromAddress, nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_invalid_response.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/lib/pq" 9 | "github.com/rss3-network/global-indexer/schema" 10 | ) 11 | 12 | type NodeInvalidResponse struct { 13 | ID uint64 `gorm:"id;primaryKey"` 14 | EpochID uint64 `gorm:"column:epoch_id"` 15 | Type schema.NodeInvalidResponseType `gorm:"column:type"` 16 | Request string `gorm:"column:request"` 17 | VerifierNodes pq.ByteaArray `gorm:"column:verifier_nodes;type:bytea[]"` 18 | VerifierResponse json.RawMessage `gorm:"column:verifier_response;type:jsonb"` 19 | Node common.Address `gorm:"column:node"` 20 | Response json.RawMessage `gorm:"column:response;type:jsonb"` 21 | CreatedAt time.Time `gorm:"column:created_at"` 22 | UpdatedAt time.Time `gorm:"column:updated_at"` 23 | } 24 | 25 | func (*NodeInvalidResponse) TableName() string { 26 | return "node_invalid_response" 27 | } 28 | 29 | func (n *NodeInvalidResponse) Import(nodeInvalidResponse *schema.NodeInvalidResponse) { 30 | n.EpochID = nodeInvalidResponse.EpochID 31 | n.Type = nodeInvalidResponse.Type 32 | n.Request = nodeInvalidResponse.Request 33 | 34 | for _, verifierNode := range nodeInvalidResponse.VerifierNodes { 35 | n.VerifierNodes = append(n.VerifierNodes, verifierNode.Bytes()) 36 | } 37 | 38 | n.VerifierResponse = nodeInvalidResponse.VerifierResponse 39 | n.Node = nodeInvalidResponse.Node 40 | n.Response = nodeInvalidResponse.Response 41 | } 42 | 43 | func (n *NodeInvalidResponse) Export() *schema.NodeInvalidResponse { 44 | var verifierNodes = make([]common.Address, len(n.VerifierNodes)) 45 | 46 | for i, verifierNode := range n.VerifierNodes { 47 | verifierNodes[i] = common.BytesToAddress(verifierNode) 48 | } 49 | 50 | return &schema.NodeInvalidResponse{ 51 | ID: n.ID, 52 | EpochID: n.EpochID, 53 | Type: n.Type, 54 | Request: n.Request, 55 | VerifierNodes: verifierNodes, 56 | VerifierResponse: n.VerifierResponse, 57 | Node: n.Node, 58 | Response: n.Response, 59 | CreatedAt: n.CreatedAt.Unix(), 60 | } 61 | } 62 | 63 | type NodeInvalidResponses []NodeInvalidResponse 64 | 65 | func (ns *NodeInvalidResponses) Import(nodeInvalidResponses []*schema.NodeInvalidResponse) { 66 | *ns = make([]NodeInvalidResponse, 0, len(nodeInvalidResponses)) 67 | 68 | for _, nodeInvalidResponse := range nodeInvalidResponses { 69 | var tNodeInvalidResponse NodeInvalidResponse 70 | 71 | tNodeInvalidResponse.Import(nodeInvalidResponse) 72 | 73 | *ns = append(*ns, tNodeInvalidResponse) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/service/scheduler/taxer/taxer.go: -------------------------------------------------------------------------------- 1 | package taxer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/ethereum/go-ethereum/ethclient" 13 | "github.com/redis/go-redis/v9" 14 | "github.com/rss3-network/global-indexer/common/txmgr" 15 | "github.com/rss3-network/global-indexer/contract/l2" 16 | stakingv2 "github.com/rss3-network/global-indexer/contract/l2/staking/v2" 17 | "github.com/rss3-network/global-indexer/internal/config" 18 | "github.com/rss3-network/global-indexer/internal/cronjob" 19 | "github.com/rss3-network/global-indexer/internal/database" 20 | "github.com/rss3-network/global-indexer/internal/service" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | var _ service.Server = (*Server)(nil) 25 | 26 | var ( 27 | Name = "taxer" 28 | Timeout = 3 * time.Minute 29 | ) 30 | 31 | type Server struct { 32 | cronJob *cronjob.CronJob 33 | databaseClient database.Client 34 | chainID *big.Int 35 | stakingContract *stakingv2.Staking 36 | settlerConfig *config.Settler 37 | txManager txmgr.TxManager 38 | } 39 | 40 | func (s *Server) Name() string { 41 | return Name 42 | } 43 | 44 | func (s *Server) Spec() string { 45 | return "*/10 * * * * *" // every 10 seconds 46 | } 47 | 48 | func (s *Server) Run(ctx context.Context) error { 49 | err := s.cronJob.AddFunc(ctx, s.Spec(), func() { 50 | if err := s.checkAndSubmitAverageTaxRate(ctx); err != nil { 51 | zap.L().Error("submit average tax rate error", zap.Error(err)) 52 | 53 | return 54 | } 55 | }) 56 | 57 | if err != nil { 58 | return fmt.Errorf("add cron job error: %w", err) 59 | } 60 | 61 | s.cronJob.Start() 62 | defer s.cronJob.Stop() 63 | 64 | stopchan := make(chan os.Signal, 1) 65 | 66 | signal.Notify(stopchan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 67 | <-stopchan 68 | 69 | return nil 70 | } 71 | 72 | func New(databaseClient database.Client, redisClient *redis.Client, ethereumClient *ethclient.Client, config *config.File, txManager *txmgr.SimpleTxManager) (*Server, error) { 73 | chainID, err := ethereumClient.ChainID(context.Background()) 74 | if err != nil { 75 | return nil, fmt.Errorf("get chain ID: %w", err) 76 | } 77 | 78 | contractAddresses := l2.ContractMap[chainID.Uint64()] 79 | if contractAddresses == nil { 80 | return nil, fmt.Errorf("contract address not found for chain id: %d", chainID.Uint64()) 81 | } 82 | 83 | stakingContract, err := stakingv2.NewStaking(contractAddresses.AddressStakingProxy, ethereumClient) 84 | if err != nil { 85 | return nil, fmt.Errorf("new staking contract: %w", err) 86 | } 87 | 88 | server := &Server{ 89 | cronJob: cronjob.New(redisClient, Name, Timeout), 90 | databaseClient: databaseClient, 91 | chainID: chainID, 92 | stakingContract: stakingContract, 93 | settlerConfig: config.Settler, 94 | txManager: txManager, 95 | } 96 | 97 | return server, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/client.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "embed" 7 | "fmt" 8 | 9 | "github.com/pressly/goose/v3" 10 | "github.com/rss3-network/global-indexer/internal/database" 11 | "go.uber.org/zap" 12 | "gorm.io/driver/postgres" 13 | "gorm.io/gorm" 14 | "moul.io/zapgorm2" 15 | ) 16 | 17 | var _ database.Client = (*client)(nil) 18 | 19 | //go:embed migration/*.sql 20 | var migrationFS embed.FS 21 | 22 | type client struct { 23 | database *gorm.DB 24 | } 25 | 26 | func (c *client) Migrate(ctx context.Context) error { 27 | goose.SetBaseFS(migrationFS) 28 | goose.SetTableName("versions") 29 | goose.SetLogger(&database.SugaredLogger{Logger: zap.L().Sugar()}) 30 | 31 | if err := goose.SetDialect(new(postgres.Dialector).Name()); err != nil { 32 | return fmt.Errorf("set migration dialect: %w", err) 33 | } 34 | 35 | connector, err := c.database.DB() 36 | if err != nil { 37 | return fmt.Errorf("get database connector: %w", err) 38 | } 39 | 40 | return goose.UpContext(ctx, connector, "migration") 41 | } 42 | 43 | func (c *client) WithTransaction(ctx context.Context, transactionFunction func(ctx context.Context, client database.Client) error, transactionOptions ...*sql.TxOptions) error { 44 | transaction, err := c.Begin(ctx, transactionOptions...) 45 | if err != nil { 46 | return fmt.Errorf("begin transaction: %w", err) 47 | } 48 | 49 | if err := transactionFunction(ctx, transaction); err != nil { 50 | _ = transaction.Rollback() 51 | 52 | return fmt.Errorf("execute transaction: %w", err) 53 | } 54 | 55 | if err := transaction.Commit(); err != nil { 56 | return fmt.Errorf("commit transaction: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (c *client) Begin(ctx context.Context, transactionOptions ...*sql.TxOptions) (database.Client, error) { 63 | transaction := c.database.WithContext(ctx).Begin(transactionOptions...) 64 | if err := transaction.Error; err != nil { 65 | return nil, fmt.Errorf("begin transaction: %w", err) 66 | } 67 | 68 | return &client{database: transaction}, nil 69 | } 70 | 71 | func (c *client) Rollback() error { 72 | return c.database.Rollback().Error 73 | } 74 | 75 | func (c *client) Commit() error { 76 | return c.database.Commit().Error 77 | } 78 | 79 | func (c *client) RollbackBlock(_ context.Context, _, _ uint64) error { 80 | // TODO implement the function. 81 | return nil 82 | } 83 | 84 | // Dial dials a database. 85 | func Dial(_ context.Context, dataSourceName string) (database.Client, error) { 86 | logger := zapgorm2.New(zap.L()) 87 | logger.SetAsDefault() 88 | 89 | config := gorm.Config{ 90 | Logger: logger, 91 | } 92 | 93 | databaseClient, err := gorm.Open(postgres.Open(dataSourceName), &config) 94 | if err != nil { 95 | return nil, fmt.Errorf("dial database: %w", err) 96 | } 97 | 98 | return &client{ 99 | database: databaseClient, 100 | }, nil 101 | } 102 | -------------------------------------------------------------------------------- /common/geolite2/client.go: -------------------------------------------------------------------------------- 1 | package geolite2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "path/filepath" 8 | 9 | "github.com/maxmind/geoipupdate/v6/pkg/geoipupdate" 10 | "github.com/oschwald/geoip2-golang" 11 | "github.com/rss3-network/global-indexer/internal/config" 12 | "github.com/rss3-network/global-indexer/schema" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type Client struct { 17 | reader *geoip2.Reader 18 | } 19 | 20 | func (c *Client) LookupNodeLocation(_ context.Context, endpoint string) ([]*schema.NodeLocation, error) { 21 | if c == nil || c.reader == nil { 22 | zap.L().Warn("geoip2 client is nil") 23 | 24 | return nil, nil 25 | } 26 | 27 | ips := make([]net.IP, 0) 28 | 29 | zap.L().Info("Looking up Node location", zap.String("endpoint", endpoint)) 30 | 31 | if ip := net.ParseIP(endpoint); ip == nil { 32 | ipAddresses, err := net.LookupIP(endpoint) 33 | if err != nil { 34 | return nil, fmt.Errorf("lookup endpoint: %w", err) 35 | } 36 | 37 | for _, ipAddress := range ipAddresses { 38 | if ipv4 := ipAddress.To4(); ipv4 != nil { 39 | ips = append(ips, ipv4) 40 | } 41 | } 42 | } else { 43 | ips = append(ips, ip) 44 | } 45 | 46 | records := make([]*schema.NodeLocation, 0, len(ips)) 47 | 48 | for _, ip := range ips { 49 | record, err := c.reader.City(ip) 50 | if err != nil { 51 | return nil, fmt.Errorf("get city: %w", err) 52 | } 53 | 54 | if record.Location.Longitude == 0 && record.Location.Latitude == 0 { 55 | continue 56 | } 57 | 58 | local := &schema.NodeLocation{ 59 | Latitude: record.Location.Latitude, 60 | Longitude: record.Location.Longitude, 61 | } 62 | 63 | if len(record.Country.Names) > 0 { 64 | local.Country = record.Country.Names["en"] 65 | } 66 | 67 | if len(record.Subdivisions) > 0 && len(record.Subdivisions[0].Names) > 0 { 68 | local.Region = record.Subdivisions[0].Names["en"] 69 | } 70 | 71 | if len(record.City.Names) > 0 { 72 | local.City = record.City.Names["en"] 73 | } 74 | 75 | records = append(records, local) 76 | } 77 | 78 | return records, nil 79 | } 80 | 81 | func NewClient(conf *config.GeoIP) *Client { 82 | dir := filepath.Dir(conf.File) 83 | 84 | config := &geoipupdate.Config{ 85 | URL: "https://updates.maxmind.com", 86 | DatabaseDirectory: dir, 87 | LockFile: filepath.Join(dir, ".geoipupdate.lock"), 88 | AccountID: conf.Account, 89 | LicenseKey: conf.LicenseKey, 90 | EditionIDs: []string{"GeoLite2-City"}, 91 | Output: true, 92 | Verbose: true, 93 | Parallelism: 1, 94 | } 95 | 96 | client := geoipupdate.NewClient(config) 97 | 98 | err := client.Run(context.Background()) 99 | if err != nil { 100 | zap.L().Warn("run geoipupdate failed", zap.Error(err)) 101 | } 102 | 103 | reader, err := geoip2.Open(conf.File) 104 | if err != nil { 105 | zap.L().Warn("open geoip2 database failed", zap.Error(err)) 106 | return nil 107 | } 108 | 109 | return &Client{ 110 | reader: reader, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /contract/l2/multicall.go: -------------------------------------------------------------------------------- 1 | package l2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/ethclient" 11 | v2 "github.com/rss3-network/global-indexer/contract/l2/staking/v2" 12 | "github.com/rss3-network/global-indexer/contract/multicall3" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type StakingV2MulticallClient struct { 17 | *v2.Staking 18 | chainID uint64 19 | ethereumClient bind.ContractCaller 20 | } 21 | 22 | type ChipInfo struct { 23 | NodeAddr common.Address 24 | Tokens *big.Int 25 | Shares *big.Int 26 | } 27 | 28 | func (client *StakingV2MulticallClient) StakingV2GetChipsInfo(ctx context.Context, blockNumber *big.Int, chipIDs []*big.Int) ([]ChipInfo, error) { 29 | abi, err := v2.StakingMetaData.GetAbi() 30 | if err != nil { 31 | return nil, fmt.Errorf("get staking contract abi: %w", err) 32 | } 33 | 34 | // Prepare the input data for the multicall 35 | calls := make([]multicall3.Multicall3Call3, 0, len(chipIDs)) 36 | 37 | for _, chipID := range chipIDs { 38 | callData, err := abi.Pack("getChipInfo", chipID) 39 | if err != nil { 40 | return nil, fmt.Errorf("pack getChipInfo: %w", err) 41 | } 42 | 43 | calls = append(calls, multicall3.Multicall3Call3{ 44 | Target: ContractMap[client.chainID].AddressStakingProxy, 45 | CallData: callData, 46 | }) 47 | } 48 | 49 | // Execute the multicall 50 | results, err := multicall3.Aggregate3(ctx, client.chainID, calls, blockNumber, client.ethereumClient) 51 | if err != nil { 52 | return nil, fmt.Errorf("multicall failed: %w", err) 53 | } 54 | 55 | chipsInfo := make([]ChipInfo, len(chipIDs)) 56 | 57 | // Process the response data 58 | for i, call := range results { 59 | if !call.Success { 60 | zap.L().Error("multicall failed", zap.String("chipID", chipIDs[i].String()), zap.Any("call", call)) 61 | 62 | return nil, fmt.Errorf("multicall failed, chip id: %s", chipIDs[i].String()) 63 | } 64 | 65 | var chipInfo ChipInfo 66 | 67 | // Unpack the returned data 68 | err := abi.UnpackIntoInterface(&chipInfo, "getChipInfo", call.ReturnData) 69 | if err != nil { 70 | zap.L().Error("unpack getChipInfo result", zap.Error(err), zap.String("chipID", chipIDs[i].String()), zap.Any("data", call.ReturnData)) 71 | 72 | return nil, fmt.Errorf("unpack getChipInfo: %w", err) 73 | } 74 | 75 | chipsInfo[i] = chipInfo 76 | } 77 | 78 | return chipsInfo, nil 79 | } 80 | 81 | func NewStakingV2MulticallClient(chainID uint64, ethereumClient *ethclient.Client) (*StakingV2MulticallClient, error) { 82 | contractAddresses := ContractMap[chainID] 83 | if contractAddresses == nil { 84 | return nil, fmt.Errorf("contract address not found for chain id: %d", chainID) 85 | } 86 | 87 | staking, err := v2.NewStaking(contractAddresses.AddressStakingProxy, ethereumClient) 88 | if err != nil { 89 | return nil, fmt.Errorf("create staking contract: %w", err) 90 | } 91 | 92 | return &StakingV2MulticallClient{ 93 | Staking: staking, 94 | chainID: chainID, 95 | ethereumClient: ethereumClient, 96 | }, nil 97 | } 98 | -------------------------------------------------------------------------------- /common/signer/transaction_args.go: -------------------------------------------------------------------------------- 1 | package signer 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/common/hexutil" 8 | "github.com/ethereum/go-ethereum/core/types" 9 | ) 10 | 11 | // TransactionArgs represents the arguments to construct a new transaction 12 | // or a message call. 13 | type TransactionArgs struct { 14 | From *common.Address `json:"from"` 15 | To *common.Address `json:"to"` 16 | Gas *hexutil.Uint64 `json:"gas"` 17 | GasPrice *hexutil.Big `json:"gasPrice"` 18 | MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` 19 | MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` 20 | Value *hexutil.Big `json:"value"` 21 | Nonce *hexutil.Uint64 `json:"nonce"` 22 | 23 | // We accept "data" and "input" for backwards-compatibility reasons. 24 | // "input" is the newer name and should be preferred by clients. 25 | // Issue detail: https://github.com/ethereum/go-ethereum/issues/15628 26 | Data *hexutil.Bytes `json:"data"` 27 | Input *hexutil.Bytes `json:"input"` 28 | 29 | AccessList *types.AccessList `json:"accessList,omitempty"` 30 | ChainID *hexutil.Big `json:"chainId,omitempty"` 31 | } 32 | 33 | // NewTransactionArgsFromTransaction creates a TransactionArgs struct from an EIP-1559 transaction 34 | func NewTransactionArgsFromTransaction(chainID *big.Int, from common.Address, tx *types.Transaction) *TransactionArgs { 35 | data := hexutil.Bytes(tx.Data()) 36 | nonce := hexutil.Uint64(tx.Nonce()) 37 | gas := hexutil.Uint64(tx.Gas()) 38 | accesses := tx.AccessList() 39 | args := &TransactionArgs{ 40 | From: &from, 41 | Input: &data, 42 | Nonce: &nonce, 43 | Value: (*hexutil.Big)(tx.Value()), 44 | Gas: &gas, 45 | To: tx.To(), 46 | ChainID: (*hexutil.Big)(chainID), 47 | MaxFeePerGas: (*hexutil.Big)(tx.GasFeeCap()), 48 | MaxPriorityFeePerGas: (*hexutil.Big)(tx.GasTipCap()), 49 | AccessList: &accesses, 50 | } 51 | 52 | return args 53 | } 54 | 55 | // data retrieves the transaction calldata. Input field is preferred. 56 | func (args *TransactionArgs) data() []byte { 57 | if args.Input != nil { 58 | return *args.Input 59 | } 60 | 61 | if args.Data != nil { 62 | return *args.Data 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // ToTransaction converts the arguments to a transaction. 69 | func (args *TransactionArgs) ToTransaction() *types.Transaction { 70 | var data types.TxData 71 | 72 | al := types.AccessList{} 73 | 74 | if args.AccessList != nil { 75 | al = *args.AccessList 76 | } 77 | 78 | data = &types.DynamicFeeTx{ 79 | To: args.To, 80 | ChainID: (*big.Int)(args.ChainID), 81 | Nonce: uint64(*args.Nonce), 82 | Gas: uint64(*args.Gas), 83 | GasFeeCap: (*big.Int)(args.MaxFeePerGas), 84 | GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas), 85 | Value: (*big.Int)(args.Value), 86 | Data: args.data(), 87 | AccessList: al, 88 | } 89 | 90 | return types.NewTx(data) 91 | } 92 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_event.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | "time" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/rss3-network/global-indexer/schema" 11 | ) 12 | 13 | type NodeEvent struct { 14 | TransactionHash string `gorm:"column:transaction_hash"` 15 | TransactionIndex uint `gorm:"column:transaction_index"` 16 | NodeID uint64 `gorm:"column:node_id"` 17 | AddressFrom common.Address `gorm:"column:address_from"` 18 | AddressTo common.Address `gorm:"column:address_to"` 19 | Type schema.NodeEventType `gorm:"column:type"` 20 | LogIndex uint `gorm:"column:log_index"` 21 | ChainID uint64 `gorm:"column:chain_id"` 22 | BlockHash string `gorm:"column:block_hash"` 23 | BlockNumber uint64 `gorm:"column:block_number"` 24 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 25 | Metadata json.RawMessage `gorm:"column:metadata"` 26 | Finalized bool `gorm:"column:finalized"` 27 | } 28 | 29 | func (*NodeEvent) TableName() string { 30 | return "node.events" 31 | } 32 | 33 | func (n *NodeEvent) Import(nodeEvent schema.NodeEvent) (err error) { 34 | n.TransactionHash = nodeEvent.TransactionHash.String() 35 | n.TransactionIndex = nodeEvent.TransactionIndex 36 | n.NodeID = nodeEvent.NodeID.Uint64() 37 | n.AddressFrom = nodeEvent.AddressFrom 38 | n.AddressTo = nodeEvent.AddressTo 39 | n.Type = nodeEvent.Type 40 | n.LogIndex = nodeEvent.LogIndex 41 | n.ChainID = nodeEvent.ChainID 42 | n.BlockHash = nodeEvent.BlockHash.String() 43 | n.BlockNumber = nodeEvent.BlockNumber.Uint64() 44 | n.BlockTimestamp = time.Unix(nodeEvent.BlockTimestamp, 0) 45 | 46 | n.Metadata, err = json.Marshal(nodeEvent.Metadata) 47 | if err != nil { 48 | return fmt.Errorf("marshal node event metadata: %w", err) 49 | } 50 | 51 | n.Finalized = nodeEvent.Finalized 52 | 53 | return nil 54 | } 55 | 56 | func (n *NodeEvent) Export() (*schema.NodeEvent, error) { 57 | nodeEvent := schema.NodeEvent{ 58 | TransactionHash: common.HexToHash(n.TransactionHash), 59 | TransactionIndex: n.TransactionIndex, 60 | NodeID: big.NewInt(int64(n.NodeID)), 61 | AddressFrom: n.AddressFrom, 62 | AddressTo: n.AddressTo, 63 | Type: n.Type, 64 | LogIndex: n.LogIndex, 65 | ChainID: n.ChainID, 66 | BlockHash: common.HexToHash(n.BlockHash), 67 | BlockNumber: big.NewInt(int64(n.BlockNumber)), 68 | BlockTimestamp: n.BlockTimestamp.Unix(), 69 | Finalized: n.Finalized, 70 | } 71 | 72 | if err := json.Unmarshal(n.Metadata, &nodeEvent.Metadata); len(n.Metadata) > 0 && err != nil { 73 | return nil, fmt.Errorf("unmarshal node event metadata: %w", err) 74 | } 75 | 76 | return &nodeEvent, nil 77 | } 78 | 79 | type NodeEvents []*NodeEvent 80 | 81 | func (n NodeEvents) Export() ([]*schema.NodeEvent, error) { 82 | nodeEvents := make([]*schema.NodeEvent, 0) 83 | 84 | for _, nodeEvent := range n { 85 | exported, err := nodeEvent.Export() 86 | if err != nil { 87 | return nil, fmt.Errorf("export node event: %w", err) 88 | } 89 | 90 | nodeEvents = append(nodeEvents, exported) 91 | } 92 | 93 | return nodeEvents, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/bridge_transaction.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | "github.com/samber/lo" 9 | "github.com/shopspring/decimal" 10 | gorm "gorm.io/gorm/schema" 11 | ) 12 | 13 | var ( 14 | _ gorm.Tabler = (*BridgeTransaction)(nil) 15 | _ schema.BridgeTransactionTransformer = (*BridgeTransaction)(nil) 16 | ) 17 | 18 | type BridgeTransaction struct { 19 | ID string `gorm:"column:id;primaryKey"` 20 | Type string `gorm:"column:type;primaryKey"` 21 | Sender string `gorm:"column:sender"` 22 | Receiver string `gorm:"column:receiver"` 23 | TokenAddressL1 *string `gorm:"column:token_address_l1"` 24 | TokenAddressL2 *string `gorm:"column:token_address_l2"` 25 | TokenValue decimal.Decimal `gorm:"column:token_value"` 26 | Data string `gorm:"column:data"` 27 | ChainID uint64 `gorm:"column:chain_id"` 28 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 29 | BlockNumber uint64 `gorm:"column:block_number"` 30 | TransactionIndex uint `gorm:"column:transaction_index"` 31 | Finalized bool `gorm:"column:finalized"` 32 | } 33 | 34 | func (b *BridgeTransaction) TableName() string { 35 | return "bridge.transactions" 36 | } 37 | 38 | func (b *BridgeTransaction) Import(bridgeTransaction schema.BridgeTransaction) error { 39 | b.ID = bridgeTransaction.ID.String() 40 | b.Type = string(bridgeTransaction.Type) 41 | b.Sender = bridgeTransaction.Sender.String() 42 | b.Receiver = bridgeTransaction.Receiver.String() 43 | b.TokenAddressL1 = lo.ToPtr(bridgeTransaction.TokenAddressL1.String()) 44 | b.TokenAddressL2 = lo.ToPtr(bridgeTransaction.TokenAddressL2.String()) 45 | b.TokenValue = decimal.NewFromBigInt(bridgeTransaction.TokenValue, 0) 46 | b.Data = bridgeTransaction.Data 47 | b.ChainID = bridgeTransaction.ChainID 48 | b.BlockTimestamp = bridgeTransaction.BlockTimestamp 49 | b.BlockNumber = bridgeTransaction.BlockNumber 50 | b.TransactionIndex = bridgeTransaction.TransactionIndex 51 | b.Finalized = bridgeTransaction.Finalized 52 | 53 | return nil 54 | } 55 | 56 | func (b *BridgeTransaction) Export() (*schema.BridgeTransaction, error) { 57 | bridgeTransaction := schema.BridgeTransaction{ 58 | ID: common.HexToHash(b.ID), 59 | Type: schema.BridgeTransactionType(b.Type), 60 | Sender: common.HexToAddress(b.Sender), 61 | Receiver: common.HexToAddress(b.Receiver), 62 | TokenAddressL1: func(tokenAddress *string) *common.Address { 63 | if tokenAddress == nil { 64 | return nil 65 | } 66 | 67 | return lo.ToPtr(common.HexToAddress(*tokenAddress)) 68 | }(b.TokenAddressL1), 69 | TokenAddressL2: func(tokenAddress *string) *common.Address { 70 | if tokenAddress == nil { 71 | return nil 72 | } 73 | 74 | return lo.ToPtr(common.HexToAddress(*tokenAddress)) 75 | }(b.TokenAddressL2), 76 | TokenValue: b.TokenValue.BigInt(), 77 | Data: b.Data, 78 | ChainID: b.ChainID, 79 | BlockTimestamp: b.BlockTimestamp, 80 | BlockNumber: b.BlockNumber, 81 | TransactionIndex: b.TransactionIndex, 82 | Finalized: b.Finalized, 83 | } 84 | 85 | return &bridgeTransaction, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/service/hub/model/dsl/decentralized.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | // ActivityRequest represents the request for an activity by its ID. 4 | type ActivityRequest struct { 5 | ID string `param:"id" validate:"required"` 6 | ActionLimit int `query:"action_limit" validate:"min=1,max=20" default:"10"` 7 | ActionPage int `query:"action_page" validate:"min=1" default:"1"` 8 | } 9 | 10 | // ActivitiesRequest represents the request for activities by an account. 11 | type ActivitiesRequest struct { 12 | Account string `param:"account" validate:"required"` 13 | Limit *int `query:"limit" validate:"min=1,max=100" default:"100"` 14 | ActionLimit *int `query:"action_limit" validate:"min=1,max=20" default:"10"` 15 | Cursor *string `query:"cursor"` 16 | SinceTimestamp *uint64 `query:"since_timestamp"` 17 | UntilTimestamp *uint64 `query:"until_timestamp"` 18 | Status *bool `query:"success"` 19 | Direction *string `query:"direction"` 20 | Network []string `query:"network"` 21 | Tag []string `query:"tag"` 22 | Type []string `query:"-"` 23 | Platform []string `query:"platform"` 24 | } 25 | 26 | // AccountsActivitiesRequest represents the request for activities by multiple accounts. 27 | type AccountsActivitiesRequest struct { 28 | Accounts []string `json:"accounts" validate:"required,max=20"` 29 | Limit int `json:"limit" validate:"min=1,max=100" default:"100"` 30 | ActionLimit int `json:"action_limit" validate:"min=1,max=20" default:"10"` 31 | Cursor *string `json:"cursor"` 32 | SinceTimestamp *uint64 `json:"since_timestamp"` 33 | UntilTimestamp *uint64 `json:"until_timestamp"` 34 | Status *bool `json:"success"` 35 | Direction *string `json:"direction"` 36 | Network []string `json:"network"` 37 | Tag []string `json:"tag"` 38 | Type []string `json:"type"` 39 | Platform []string `json:"platform"` 40 | } 41 | 42 | // NetworkActivitiesRequest represents the request for activities by a network. 43 | type NetworkActivitiesRequest struct { 44 | Network string `param:"network" validate:"required"` 45 | 46 | Limit int `query:"limit" validate:"min=1,max=100" default:"100"` 47 | ActionLimit int `query:"action_limit" validate:"min=1,max=20" default:"10"` 48 | Cursor *string `query:"cursor"` 49 | SinceTimestamp *uint64 `query:"since_timestamp"` 50 | UntilTimestamp *uint64 `query:"until_timestamp"` 51 | Status *bool `query:"success"` 52 | Direction *string `query:"direction"` 53 | Tag []string `query:"tag"` 54 | Type []string `query:"-"` 55 | Platform []string `query:"platform"` 56 | } 57 | 58 | // PlatformActivitiesRequest represents the request for activities by a platform. 59 | type PlatformActivitiesRequest struct { 60 | Platform string `param:"platform" validate:"required"` 61 | 62 | Limit int `query:"limit" validate:"min=1,max=100" default:"50"` 63 | ActionLimit int `query:"action_limit" validate:"min=1,max=20" default:"10"` 64 | Cursor *string `query:"cursor"` 65 | SinceTimestamp *uint64 `query:"since_timestamp"` 66 | UntilTimestamp *uint64 `query:"until_timestamp"` 67 | Status *bool `query:"success"` 68 | Direction *string `query:"direction"` 69 | Tag []string `query:"tag"` 70 | Type []string `query:"-"` 71 | Network []string `query:"network"` 72 | } 73 | -------------------------------------------------------------------------------- /internal/nameresolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package nameresolver_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/rss3-network/global-indexer/internal/config" 10 | "github.com/rss3-network/global-indexer/internal/nameresolver" 11 | ) 12 | 13 | func Test_Resolve(t *testing.T) { 14 | t.Parallel() 15 | 16 | resolverConfig := &config.RPC{ 17 | RPCNetwork: &config.RPCNetwork{ 18 | Ethereum: &config.RPCEndpoint{ 19 | Endpoint: "https://rpc.ankr.com/eth", 20 | }, 21 | Crossbell: &config.RPCEndpoint{ 22 | Endpoint: "https://rpc.crossbell.io", 23 | }, 24 | Polygon: &config.RPCEndpoint{ 25 | Endpoint: "https://rpc.ankr.com/polygon", 26 | }, 27 | //Farcaster: &config.RPCEndpoint{ 28 | // Endpoint: "https://nemes.farcaster.xyz:2281", 29 | //}, 30 | }, 31 | } 32 | 33 | nr, _ := nameresolver.NewNameResolver(context.Background(), resolverConfig.RPCNetwork) 34 | 35 | type arguments struct { 36 | ns string 37 | } 38 | 39 | tests := []struct { 40 | name string 41 | input arguments 42 | output string 43 | err error 44 | }{ 45 | { 46 | name: "unregister eth", 47 | input: arguments{"qwerfdsazxcv.eth"}, 48 | output: "", 49 | err: fmt.Errorf("%s", nameresolver.ErrUnregisterName), 50 | }, 51 | { 52 | name: "unregister csb", 53 | input: arguments{"qwerfdsazxcv.csb"}, 54 | output: "", 55 | err: fmt.Errorf("%s", nameresolver.ErrUnregisterName), 56 | }, 57 | { 58 | name: "unregister lens", 59 | input: arguments{"qwerfdsazxcv.lens"}, 60 | output: "", 61 | err: fmt.Errorf("%s", nameresolver.ErrUnregisterName), 62 | }, 63 | //{ 64 | // name: "unregister farcaster", 65 | // input: arguments{"qwerfdsazxcv.fc"}, 66 | // output: "", 67 | // err: fmt.Errorf("%s", nameresolver.ErrUnregisterName), 68 | //}, 69 | { 70 | name: "unsupport name service .xxx", 71 | input: arguments{"qwerfdsazxcv.xxx"}, 72 | output: "", 73 | err: fmt.Errorf("%s:%s", nameresolver.ErrUnSupportName, "qwerfdsazxcv.xxx"), 74 | }, 75 | { 76 | name: "resolve ens", 77 | input: arguments{"vitalik.eth"}, 78 | output: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", 79 | err: nil, 80 | }, 81 | { 82 | name: "resolve csb", 83 | input: arguments{"brucexx.csb"}, 84 | output: "0x23c46e912b34C09c4bCC97F4eD7cDd762cee408A", 85 | err: nil, 86 | }, 87 | { 88 | name: "resolve lens", 89 | input: arguments{"diygod.lens"}, 90 | output: "0xc8b960d09c0078c18dcbe7eb9ab9d816bcca8944", 91 | err: nil, 92 | }, 93 | //{ 94 | // name: "resolve fc", 95 | // input: arguments{"brucexc.fc"}, 96 | // output: "0xe5d6216f0085a7f6b9b692e06cf5856e6fa41b55", 97 | // err: nil, 98 | //}, 99 | } 100 | 101 | for _, tt := range tests { 102 | tt := tt 103 | 104 | t.Run(tt.name, func(t *testing.T) { 105 | t.Parallel() 106 | 107 | output, err := nr.Resolve(context.Background(), tt.input.ns) 108 | if tt.err == nil { 109 | if err != nil { 110 | t.Fatalf("unexpected error %v", err) 111 | } 112 | 113 | if !strings.EqualFold(tt.output, output) { 114 | t.Errorf("Failure: %v => %v (expected %v)\n", tt.input, output, tt.output) 115 | } 116 | } else { 117 | if err == nil { 118 | t.Fatalf("missing expected error") 119 | } 120 | 121 | if !strings.Contains(err.Error(), tt.err.Error()) { 122 | t.Fatalf("Failure: unexpected error value %v, (expected %v)\n", err, tt.err) 123 | } 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/rss3-network/global-indexer/internal/config" 9 | "github.com/rss3-network/global-indexer/internal/config/flag" 10 | "github.com/rss3-network/global-indexer/internal/service" 11 | "github.com/rss3-network/global-indexer/internal/service/hub" 12 | "github.com/rss3-network/global-indexer/internal/service/indexer" 13 | "github.com/rss3-network/global-indexer/internal/service/scheduler" 14 | "github.com/rss3-network/global-indexer/internal/service/settler" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | "go.uber.org/fx" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | var command = cobra.Command{ 22 | SilenceUsage: true, 23 | SilenceErrors: true, 24 | PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { 25 | return viper.BindPFlags(cmd.Flags()) 26 | }, 27 | RunE: func(cmd *cobra.Command, _ []string) error { 28 | server := service.NewServer( 29 | hub.Module, 30 | fx.Provide(hub.NewServer), 31 | ) 32 | 33 | if err := server.Start(cmd.Context()); err != nil { 34 | return fmt.Errorf("start server: %w", err) 35 | } 36 | 37 | server.Wait() 38 | 39 | return nil 40 | }, 41 | } 42 | 43 | var indexCommand = &cobra.Command{ 44 | Use: "index", 45 | RunE: func(cmd *cobra.Command, _ []string) error { 46 | server := service.NewServer( 47 | indexer.Module, 48 | fx.Provide(indexer.NewServer), 49 | ) 50 | 51 | if err := server.Start(cmd.Context()); err != nil { 52 | return fmt.Errorf("start server: %w", err) 53 | } 54 | 55 | server.Wait() 56 | 57 | return nil 58 | }, 59 | } 60 | 61 | var schedulerCommand = &cobra.Command{ 62 | Use: "scheduler", 63 | RunE: func(cmd *cobra.Command, _ []string) error { 64 | server := service.NewServer( 65 | scheduler.Module, 66 | fx.Provide(scheduler.NewServer), 67 | ) 68 | 69 | if err := server.Start(cmd.Context()); err != nil { 70 | return fmt.Errorf("start server: %w", err) 71 | } 72 | 73 | server.Wait() 74 | 75 | return nil 76 | }, 77 | } 78 | 79 | var settlerCommand = &cobra.Command{ 80 | Use: "settler", 81 | RunE: func(cmd *cobra.Command, _ []string) error { 82 | server := service.NewServer( 83 | settler.Module, 84 | fx.Provide(settler.NewServer), 85 | ) 86 | 87 | if err := server.Start(cmd.Context()); err != nil { 88 | return fmt.Errorf("start server: %w", err) 89 | } 90 | 91 | server.Wait() 92 | 93 | return nil 94 | }, 95 | } 96 | 97 | func initializeLogger() { 98 | if os.Getenv(config.Environment) == config.EnvironmentDevelopment { 99 | zap.ReplaceGlobals(zap.Must(zap.NewDevelopment())) 100 | } else { 101 | zap.ReplaceGlobals(zap.Must(zap.NewProduction())) 102 | } 103 | } 104 | 105 | func init() { 106 | initializeLogger() 107 | 108 | command.AddCommand(indexCommand) 109 | command.AddCommand(schedulerCommand) 110 | command.AddCommand(settlerCommand) 111 | 112 | command.PersistentFlags().String(flag.KeyConfig, "./deploy/config.yaml", "config file path") 113 | command.PersistentFlags().Uint64(flag.KeyChainIDL1, flag.ValueChainIDL1, "l1 chain id") 114 | command.PersistentFlags().Uint64(flag.KeyChainIDL2, flag.ValueChainIDL2, "l2 chain id") 115 | 116 | indexCommand.PersistentFlags().String(flag.KeyConfig, "./deploy/config.yaml", "config file path") 117 | schedulerCommand.PersistentFlags().String(flag.KeyConfig, "./deploy/config.yaml", "config file path") 118 | schedulerCommand.PersistentFlags().String(flag.KeyServer, "detector", "server name") 119 | settlerCommand.PersistentFlags().String(flag.KeyConfig, "./deploy/config.yaml", "config file path") 120 | } 121 | 122 | func main() { 123 | if err := command.ExecuteContext(context.Background()); err != nil { 124 | zap.L().Fatal("execute command", zap.Error(err)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /common/httputil/http.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/avast/retry-go/v4" 13 | "github.com/samber/lo" 14 | ) 15 | 16 | var ( 17 | ErrorNoResults = errors.New("no results") 18 | ErrorManuallyCanceled = errors.New("request was manually canceled") 19 | ErrorTimeout = errors.New("request timed out") 20 | ) 21 | 22 | const ( 23 | DefaultTimeout = 30 * time.Second 24 | DefaultAttempts = 3 25 | ) 26 | 27 | type Client interface { 28 | FetchWithMethod(ctx context.Context, method, path, authorization string, body io.Reader) (io.ReadCloser, error) 29 | } 30 | 31 | var _ Client = (*httpClient)(nil) 32 | 33 | type httpClient struct { 34 | httpClient *http.Client 35 | attempts uint 36 | } 37 | 38 | func (h *httpClient) FetchWithMethod(ctx context.Context, method, path, authorization string, body io.Reader) (readCloser io.ReadCloser, err error) { 39 | var bodyBytes []byte 40 | // Read the body into a byte slice to be able to retry the request 41 | if body != nil { 42 | bodyBytes, _ = io.ReadAll(body) 43 | } 44 | 45 | retryableFunc := func() error { 46 | readCloser, err = h.fetchWithMethod(ctx, method, path, authorization, bytes.NewReader(bodyBytes)) 47 | return err 48 | } 49 | 50 | retryIfFunc := func(err error) bool { 51 | nonRetryableErrors := []error{ 52 | ErrorNoResults, 53 | ErrorManuallyCanceled, 54 | ErrorTimeout, 55 | } 56 | 57 | for _, nonRetryableErr := range nonRetryableErrors { 58 | if errors.Is(err, nonRetryableErr) { 59 | return false 60 | } 61 | } 62 | 63 | return true 64 | } 65 | 66 | if err = retry.Do(retryableFunc, retry.Attempts(h.attempts), retry.RetryIf(retryIfFunc)); err != nil { 67 | return nil, err 68 | } 69 | 70 | return readCloser, nil 71 | } 72 | 73 | func (h *httpClient) fetchWithMethod(ctx context.Context, method, path, authorization string, body io.Reader) (io.ReadCloser, error) { 74 | request, err := http.NewRequestWithContext(ctx, method, path, body) 75 | if err != nil { 76 | return nil, fmt.Errorf("new request: %w", err) 77 | } 78 | 79 | if method == http.MethodPost { 80 | request.Header.Set("Content-Type", "application/json") 81 | } 82 | 83 | if authorization != "" { 84 | request.Header.Set("Authorization", authorization) 85 | } 86 | 87 | response, err := h.httpClient.Do(request) 88 | if err != nil { 89 | if cause := context.Cause(ctx); errors.Is(cause, context.Canceled) { 90 | return nil, ErrorManuallyCanceled 91 | } else if errors.Is(cause, context.DeadlineExceeded) { 92 | return nil, ErrorTimeout 93 | } 94 | 95 | return nil, fmt.Errorf("send request: %w", err) 96 | } 97 | 98 | if response.StatusCode != http.StatusOK { 99 | defer lo.Try(response.Body.Close) 100 | 101 | return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) 102 | } 103 | 104 | return response.Body, nil 105 | } 106 | 107 | func NewHTTPClient(options ...ClientOption) (Client, error) { 108 | instance := httpClient{ 109 | httpClient: &http.Client{ 110 | Timeout: DefaultTimeout, 111 | }, 112 | attempts: DefaultAttempts, 113 | } 114 | 115 | for _, option := range options { 116 | if err := option(&instance); err != nil { 117 | return nil, fmt.Errorf("apply options: %w", err) 118 | } 119 | } 120 | 121 | return &instance, nil 122 | } 123 | 124 | type ClientOption func(*httpClient) error 125 | 126 | func WithAttempts(attempts uint) ClientOption { 127 | return func(h *httpClient) error { 128 | h.attempts = attempts 129 | 130 | return nil 131 | } 132 | } 133 | 134 | func WithTimeout(timeout time.Duration) ClientOption { 135 | return func(h *httpClient) error { 136 | h.httpClient.Timeout = timeout 137 | 138 | return nil 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/node_stat.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/rss3-network/global-indexer/schema" 8 | ) 9 | 10 | type Stat struct { 11 | Address common.Address `gorm:"column:address;primaryKey"` 12 | Endpoint string `gorm:"column:endpoint"` 13 | AccessToken string `gorm:"column:access_token"` 14 | Points float64 `gorm:"column:points"` 15 | IsPublicGood bool `gorm:"column:is_public_good"` 16 | IsFullNode bool `gorm:"column:is_full_node"` 17 | IsRssNode bool `gorm:"column:is_rss_node"` 18 | IsAINode bool `gorm:"column:is_ai_node"` 19 | Staking float64 `gorm:"column:staking"` 20 | Epoch int64 `gorm:"column:epoch"` 21 | TotalRequest int64 `gorm:"column:total_request_count"` 22 | EpochRequest int64 `gorm:"column:epoch_request_count"` 23 | EpochInvalidRequest int64 `gorm:"column:epoch_invalid_request_count"` 24 | DecentralizedNetwork int `gorm:"column:decentralized_network_count"` 25 | FederatedNetwork int `gorm:"column:federated_network_count"` 26 | Indexer int `gorm:"column:indexer_count"` 27 | ResetAt time.Time `gorm:"column:reset_at"` 28 | CreatedAt time.Time `gorm:"column:created_at"` 29 | UpdatedAt time.Time `gorm:"column:updated_at"` 30 | } 31 | 32 | func (*Stat) TableName() string { 33 | return "node_stat" 34 | } 35 | 36 | func (s *Stat) Import(stat *schema.Stat) (err error) { 37 | s.Address = stat.Address 38 | s.Endpoint = stat.Endpoint 39 | s.AccessToken = stat.AccessToken 40 | s.Points = stat.Score 41 | s.IsPublicGood = stat.IsPublicGood 42 | s.IsFullNode = stat.IsFullNode 43 | s.IsRssNode = stat.IsRssNode 44 | s.IsAINode = stat.IsAINode 45 | s.Staking = stat.Staking 46 | s.Epoch = stat.Epoch 47 | s.TotalRequest = stat.TotalRequest 48 | s.EpochRequest = stat.EpochRequest 49 | s.EpochInvalidRequest = stat.EpochInvalidRequest 50 | s.DecentralizedNetwork = stat.DecentralizedNetwork 51 | s.FederatedNetwork = stat.FederatedNetwork 52 | s.Indexer = stat.Indexer 53 | s.ResetAt = stat.ResetAt 54 | 55 | return nil 56 | } 57 | 58 | func (s *Stat) Export() (*schema.Stat, error) { 59 | stat := schema.Stat{ 60 | Address: s.Address, 61 | Endpoint: s.Endpoint, 62 | AccessToken: s.AccessToken, 63 | Score: s.Points, 64 | IsPublicGood: s.IsPublicGood, 65 | IsFullNode: s.IsFullNode, 66 | IsRssNode: s.IsRssNode, 67 | IsAINode: s.IsAINode, 68 | Staking: s.Staking, 69 | Epoch: s.Epoch, 70 | TotalRequest: s.TotalRequest, 71 | EpochRequest: s.EpochRequest, 72 | EpochInvalidRequest: s.EpochInvalidRequest, 73 | DecentralizedNetwork: s.DecentralizedNetwork, 74 | FederatedNetwork: s.FederatedNetwork, 75 | Indexer: s.Indexer, 76 | ResetAt: s.ResetAt, 77 | } 78 | 79 | return &stat, nil 80 | } 81 | 82 | type Stats []Stat 83 | 84 | func (s *Stats) Export() ([]*schema.Stat, error) { 85 | stats := make([]*schema.Stat, 0) 86 | 87 | for _, stat := range *s { 88 | exportedStat, err := stat.Export() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | stats = append(stats, exportedStat) 94 | } 95 | 96 | return stats, nil 97 | } 98 | 99 | func (s *Stats) Import(stats []*schema.Stat) (err error) { 100 | *s = make([]Stat, 0, len(stats)) 101 | 102 | for _, stat := range stats { 103 | var tStat Stat 104 | 105 | if err = tStat.Import(stat); err != nil { 106 | return err 107 | } 108 | 109 | *s = append(*s, tStat) 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /internal/database/dialer/postgres/table/epoch.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/rss3-network/global-indexer/schema" 9 | "github.com/shopspring/decimal" 10 | ) 11 | 12 | type Epoch struct { 13 | ID uint64 `gorm:"column:id;primaryKey"` 14 | StartTimestamp time.Time `gorm:"column:start_timestamp"` 15 | EndTimestamp time.Time `gorm:"column:end_timestamp"` 16 | TransactionHash string `gorm:"column:transaction_hash"` 17 | TransactionIndex uint `gorm:"column:transaction_index"` 18 | BlockHash string `gorm:"column:block_hash"` 19 | BlockNumber uint64 `gorm:"column:block_number"` 20 | BlockTimestamp time.Time `gorm:"column:block_timestamp"` 21 | TotalOperationRewards decimal.Decimal `gorm:"column:total_operation_rewards"` 22 | TotalStakingRewards decimal.Decimal `gorm:"column:total_staking_rewards"` 23 | TotalRewardedNodes int `gorm:"column:total_rewarded_nodes"` 24 | TotalRequestCounts decimal.Decimal `gorm:"column:total_request_counts"` 25 | Finalized bool `gorm:"column:finalized"` 26 | 27 | CreatedAt time.Time `gorm:"column:created_at"` 28 | UpdatedAt time.Time `gorm:"column:updated_at"` 29 | } 30 | 31 | func (e *Epoch) TableName() string { 32 | return "epoch" 33 | } 34 | 35 | func (e *Epoch) Import(epoch *schema.Epoch) error { 36 | e.ID = epoch.ID 37 | e.StartTimestamp = time.Unix(epoch.StartTimestamp, 0) 38 | e.EndTimestamp = time.Unix(epoch.EndTimestamp, 0) 39 | e.TransactionHash = epoch.TransactionHash.String() 40 | e.TransactionIndex = epoch.TransactionIndex 41 | e.BlockHash = epoch.BlockHash.String() 42 | e.BlockNumber = epoch.BlockNumber.Uint64() 43 | e.BlockTimestamp = time.Unix(epoch.BlockTimestamp, 0) 44 | e.TotalOperationRewards = epoch.TotalOperationRewards 45 | e.TotalStakingRewards = epoch.TotalStakingRewards 46 | e.TotalRewardedNodes = epoch.TotalRewardedNodes 47 | e.TotalRequestCounts = epoch.TotalRequestCounts 48 | e.Finalized = epoch.Finalized 49 | 50 | return nil 51 | } 52 | 53 | func (e *Epoch) Export(epochItems []*schema.RewardedNode) (*schema.Epoch, error) { 54 | epoch := schema.Epoch{ 55 | ID: e.ID, 56 | StartTimestamp: e.StartTimestamp.Unix(), 57 | EndTimestamp: e.EndTimestamp.Unix(), 58 | TransactionHash: common.HexToHash(e.TransactionHash), 59 | TransactionIndex: e.TransactionIndex, 60 | BlockTimestamp: e.BlockTimestamp.Unix(), 61 | BlockHash: common.HexToHash(e.BlockHash), 62 | BlockNumber: new(big.Int).SetUint64(e.BlockNumber), 63 | TotalOperationRewards: e.TotalOperationRewards, 64 | TotalStakingRewards: e.TotalStakingRewards, 65 | TotalRewardedNodes: e.TotalRewardedNodes, 66 | TotalRequestCounts: e.TotalRequestCounts, 67 | RewardedNodes: epochItems, 68 | Finalized: e.Finalized, 69 | } 70 | 71 | return &epoch, nil 72 | } 73 | 74 | type Epochs []*Epoch 75 | 76 | func (e *Epochs) Export(epochItems []*schema.RewardedNode) ([]*schema.Epoch, error) { 77 | if len(*e) == 0 { 78 | return nil, nil 79 | } 80 | 81 | itemsMap := make(map[common.Hash][]*schema.RewardedNode, len(epochItems)) 82 | 83 | for _, item := range epochItems { 84 | if _, ok := itemsMap[item.TransactionHash]; !ok { 85 | itemsMap[item.TransactionHash] = make([]*schema.RewardedNode, 0, 1) 86 | } 87 | 88 | itemsMap[item.TransactionHash] = append(itemsMap[item.TransactionHash], item) 89 | } 90 | 91 | epochs := make([]*schema.Epoch, 0, len(*e)) 92 | 93 | for _, epoch := range *e { 94 | epoch, err := epoch.Export(itemsMap[common.HexToHash(epoch.TransactionHash)]) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | epochs = append(epochs, epoch) 100 | } 101 | 102 | return epochs, nil 103 | } 104 | --------------------------------------------------------------------------------