From 72990ba74d3a9c8a5b4aa5dd9a92d2671e59384c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 07:07:10 +0800 Subject: [PATCH 001/120] [fetcher] getLastSeenAccountState: do not count states --- internal/app/fetcher/account.go | 1 + internal/core/filter/account.go | 2 ++ internal/core/repository/account/filter.go | 14 ++++++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index ff49801b..7cdb6399 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -26,6 +26,7 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l Addresses: []*addr.Address{&a}, Order: "DESC", AfterTxLT: &lastLT, + NoCount: true, Limit: 1, } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index 938151bb..91d6032c 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -41,6 +41,8 @@ type AccountsReq struct { ExcludeColumn []string // TODO: support relations + NoCount bool + Order string `form:"order"` // ASC, DESC AfterTxLT *uint64 `form:"after"` diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 60d5d56d..43daf8f5 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -304,12 +304,14 @@ func (r *Repository) FilterAccounts(ctx context.Context, f *filter.AccountsReq) f.Limit = 3 } - res.Total, err = r.countAccountStates(ctx, f) - if err != nil && !errors.Is(err, core.ErrNotImplemented) { - return res, errors.Wrap(err, "count account states") - } - if res.Total == 0 && !errors.Is(err, core.ErrNotImplemented) { - return res, nil + if !f.NoCount { + res.Total, err = r.countAccountStates(ctx, f) + if err != nil && !errors.Is(err, core.ErrNotImplemented) { + return res, errors.Wrap(err, "count account states") + } + if res.Total == 0 && !errors.Is(err, core.ErrNotImplemented) { + return res, nil + } } res.Rows, err = r.filterAccountStates(ctx, f, res.Total) From 8a3685c77e8bb35cf9d49778a584c55c3eb6184a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 14:58:15 +0800 Subject: [PATCH 002/120] [indexer] getMessageSource: fix panic message --- internal/app/indexer/save.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 04d15f7d..f2fb730b 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -139,7 +139,7 @@ func (s *Service) getMessageSource(ctx context.Context, msg *core.Message) (skip return false } if err != nil && !errors.Is(err, core.ErrNotFound) { - panic(errors.Wrapf(err, "get message with hash %s", msg.Hash)) + panic(errors.Wrapf(err, "get message with hash %x", msg.Hash)) } // some masterchain messages does not have source From 1e3ab2d33ce09ba060816b8a6e76d8f772c06280 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 15:02:26 +0800 Subject: [PATCH 003/120] [parser] fix error wrappings on failed GetMethodDescription --- internal/app/parser/get.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index a8003f97..c580f381 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -152,7 +152,7 @@ func mapContentDataNFT(ret *core.AccountState, c any) { func (s *Service) getNFTItemContent(ctx context.Context, collection *core.AccountState, idx *big.Int, itemContent *cell.Cell, acc *core.AccountState) { desc, err := s.ContractRepo.GetMethodDescription(ctx, known.NFTCollection, "get_nft_content") if err != nil { - panic("get 'get_nft_content' method description") + panic(fmt.Errorf("get 'get_nft_content' method description: %w", err)) } args := []any{idx.Bytes(), itemContent} @@ -199,7 +199,7 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, idx *big.Int, item *core.AccountState) { desc, err := s.ContractRepo.GetMethodDescription(ctx, known.NFTCollection, "get_nft_address_by_index") if err != nil { - panic("get 'get_nft_address_by_index' method description") + panic(fmt.Errorf("get 'get_nft_address_by_index' method description: %w", err)) } args := []any{idx.Bytes()} @@ -210,7 +210,7 @@ func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountState, ownerAddr *addr.Address, walletAcc *core.AccountState) { desc, err := s.ContractRepo.GetMethodDescription(ctx, known.JettonMinter, "get_wallet_address") if err != nil { - panic("get 'get_wallet_address' method description") + panic(fmt.Errorf("get 'get_wallet_address' method description: %w", err)) } args := []any{ownerAddr.MustToTonutils()} @@ -236,7 +236,7 @@ func (s *Service) checkDeDustMinter(ctx context.Context, acc *core.AccountState, desc, err := s.ContractRepo.GetMethodDescription(ctx, known.DedustV2Factory, "get_pool_address") if err != nil { - panic("get 'get_pool_address' method description") + panic(fmt.Errorf("get 'get_pool_address' method description: %w", err)) } asset0 := acc.ExecutedGetMethods[known.DedustV2Pool][0].Returns[0].(*abi.DedustAsset) //nolint:forcetypeassert // that's ok @@ -266,7 +266,7 @@ func (s *Service) checkStonFiMinter(ctx context.Context, acc *core.AccountState, desc, err := s.ContractRepo.GetMethodDescription(ctx, known.StonFiRouter, "get_pool_address") if err != nil { - panic("get 'get_pool_address' method description") + panic(fmt.Errorf("get 'get_pool_address' method description: %w", err)) } asset0 := acc.ExecutedGetMethods[known.StonFiPool][0].Returns[2].(*address.Address) //nolint:forcetypeassert // that's ok From ba3690a35a8100b8d5c55fb966833403144478bd Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 15:57:51 +0800 Subject: [PATCH 004/120] docker-compose.yml: restart services on failure --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index a18764dc..177143b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: "3.5" x-anton-service: &anton-service image: "${IMAGE_NAME:-tonindexer/anton}:${IMAGE_TAG:-latest}" + restart: on-failure networks: - indexer_network depends_on: &anton-deps @@ -63,6 +64,7 @@ services: command: ["migrate", "up", "--init"] clickhouse: image: "clickhouse/clickhouse-server:22" + restart: on-failure healthcheck: test: wget --spider --no-verbose --tries=1 localhost:8123/ping || exit 1 interval: 5s @@ -87,6 +89,7 @@ services: CLICKHOUSE_PASSWORD: "${DB_PASSWORD}" postgres: image: "postgres:15" + restart: on-failure healthcheck: test: pg_isready -U "${DB_USERNAME}" -d "${DB_NAME}" || exit 1 interval: 5s @@ -107,6 +110,8 @@ services: networks: indexer_network: + driver_opts: + com.docker.network.bridge.name: br_indexer volumes: idx_ch_data: From 6b3c2152d405f32f418fef8d0803b44dbf020f9a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 16:00:32 +0800 Subject: [PATCH 005/120] [parser] add semaphore to limit maximum number of account parsing workers --- cmd/indexer/indexer.go | 5 +++-- cmd/rescan/rescan.go | 5 +++-- docker-compose.yml | 2 ++ internal/app/fetcher/fetcher_test.go | 5 +++-- internal/app/parser.go | 2 ++ internal/app/parser/account.go | 9 +++++++++ internal/app/parser/parser.go | 3 +++ internal/app/parser/parser_test.go | 5 +++-- 8 files changed, 28 insertions(+), 8 deletions(-) diff --git a/cmd/indexer/indexer.go b/cmd/indexer/indexer.go index 50846da8..5e9b4e86 100644 --- a/cmd/indexer/indexer.go +++ b/cmd/indexer/indexer.go @@ -173,8 +173,9 @@ var Command = &cli.Command{ } p := parser.NewService(&app.ParserConfig{ - BlockchainConfig: bcConfig, - ContractRepo: contractRepo, + BlockchainConfig: bcConfig, + ContractRepo: contractRepo, + MaxAccountParsingWorkers: env.GetInt("MAX_ACCOUNT_PARSING_WORKERS", 96), }) f := fetcher.NewService(&app.FetcherConfig{ API: api, diff --git a/cmd/rescan/rescan.go b/cmd/rescan/rescan.go index f746f101..95fb7161 100644 --- a/cmd/rescan/rescan.go +++ b/cmd/rescan/rescan.go @@ -76,8 +76,9 @@ var Command = &cli.Command{ } p := parser.NewService(&app.ParserConfig{ - BlockchainConfig: bcConfig, - ContractRepo: contractRepo, + BlockchainConfig: bcConfig, + ContractRepo: contractRepo, + MaxAccountParsingWorkers: env.GetInt("MAX_ACCOUNT_PARSING_WORKERS", 96), }) i := rescan.NewService(&app.RescanConfig{ ContractRepo: contractRepo, diff --git a/docker-compose.yml b/docker-compose.yml index 177143b8..4197ab0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: <<: *anton-env FROM_BLOCK: ${FROM_BLOCK} WORKERS: ${WORKERS} + MAX_ACCOUNT_PARSING_WORKERS: ${MAX_ACCOUNT_PARSING_WORKERS} LITESERVERS: ${LITESERVERS} DEBUG_LOGS: ${DEBUG_LOGS} rescan: @@ -42,6 +43,7 @@ services: <<: *anton-env RESCAN_WORKERS: ${RESCAN_WORKERS} RESCAN_SELECT_LIMIT: ${RESCAN_SELECT_LIMIT} + MAX_ACCOUNT_PARSING_WORKERS: ${MAX_ACCOUNT_PARSING_WORKERS} LITESERVERS: ${LITESERVERS} DEBUG_LOGS: ${DEBUG_LOGS} web: diff --git a/internal/app/fetcher/fetcher_test.go b/internal/app/fetcher/fetcher_test.go index c9cd9e14..886bed14 100644 --- a/internal/app/fetcher/fetcher_test.go +++ b/internal/app/fetcher/fetcher_test.go @@ -35,8 +35,9 @@ func init() { func newService(t *testing.T) *Service { p := parser.NewService(&app.ParserConfig{ - BlockchainConfig: bcConfig, - ContractRepo: nil, + BlockchainConfig: bcConfig, + ContractRepo: nil, + MaxAccountParsingWorkers: 96, }) client := liteclient.NewConnectionPool() diff --git a/internal/app/parser.go b/internal/app/parser.go index d42e2e6c..970ec759 100644 --- a/internal/app/parser.go +++ b/internal/app/parser.go @@ -23,6 +23,8 @@ var ( type ParserConfig struct { BlockchainConfig *cell.Cell ContractRepo core.ContractRepository + + MaxAccountParsingWorkers int } func GetBlockchainConfig(ctx context.Context, api ton.APIClientWrapped) (*cell.Cell, error) { diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 1d495e8b..4bc4e42e 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -106,6 +106,9 @@ func (s *Service) ParseAccountData( return errors.Wrap(app.ErrImpossibleParsing, "no contract repository") } + s.accountParseSemaphore <- struct{}{} + defer func() { <-s.accountParseSemaphore }() + interfaces, err := s.determineInterfaces(ctx, acc) if err != nil { return errors.Wrapf(err, "determine contract interfaces") @@ -135,6 +138,9 @@ func (s *Service) ParseAccountContractData( return app.ErrUnmatchedContractInterface } + s.accountParseSemaphore <- struct{}{} + defer func() { <-s.accountParseSemaphore }() + var contractTypeSet bool for _, t := range acc.Types { if t == contractDesc.Name { @@ -167,6 +173,9 @@ func (s *Service) ExecuteAccountGetMethod( return errors.Wrap(app.ErrImpossibleParsing, "no contract repository") } + s.accountParseSemaphore <- struct{}{} + defer func() { <-s.accountParseSemaphore }() + interfaces, err := s.determineInterfaces(ctx, acc) if err != nil { return errors.Wrapf(err, "determine contract interfaces") diff --git a/internal/app/parser/parser.go b/internal/app/parser/parser.go index a9630e29..a9fc5798 100644 --- a/internal/app/parser/parser.go +++ b/internal/app/parser/parser.go @@ -11,6 +11,8 @@ var _ app.ParserService = (*Service)(nil) type Service struct { *app.ParserConfig + accountParseSemaphore chan struct{} + bcConfigBase64 string } @@ -18,5 +20,6 @@ func NewService(cfg *app.ParserConfig) *Service { s := new(Service) s.ParserConfig = cfg s.bcConfigBase64 = base64.StdEncoding.EncodeToString(cfg.BlockchainConfig.ToBOC()) + s.accountParseSemaphore = make(chan struct{}, cfg.MaxAccountParsingWorkers) return s } diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index 77397df4..389307bd 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -229,7 +229,8 @@ func newService(t *testing.T) *Service { } return NewService(&app.ParserConfig{ - BlockchainConfig: bcConfig, - ContractRepo: contractRepo, + BlockchainConfig: bcConfig, + ContractRepo: contractRepo, + MaxAccountParsingWorkers: 96, }) } From 7353f9202519e8a8fd34aa460d9ae132b452ed11 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 16:16:10 +0800 Subject: [PATCH 006/120] [repo] contract: do not remove standard minter get-method descriptions --- internal/core/repository/contract/cache.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/core/repository/contract/cache.go b/internal/core/repository/contract/cache.go index 900e67db..5a12f060 100644 --- a/internal/core/repository/contract/cache.go +++ b/internal/core/repository/contract/cache.go @@ -5,10 +5,11 @@ import ( "time" "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/known" "github.com/tonindexer/anton/internal/core" ) -var cacheInvalidation = 10 * time.Second +var cacheInvalidation = 60 * time.Second type cache struct { interfaces []*core.ContractInterface @@ -31,7 +32,17 @@ func (c *cache) clearCaches() { } c.interfaces = nil c.operations = nil - c.getMethods = map[abi.ContractName]map[string]abi.GetMethodDesc{} + for i, im := range c.getMethods { + for gm := range im { + switch { + case i == known.NFTCollection && (gm == "get_nft_content" || gm == "get_nft_address_by_index"), + i == known.JettonMinter && gm == "get_wallet_address", + (i == known.DedustV2Factory || i == known.StonFiRouter) && gm == "get_pool_address": + continue + } + delete(im, gm) + } + } c.lastCleared = time.Now() } From 800906b748c611b28e3c8a68ae009ef4e8276a95 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 16:40:36 +0800 Subject: [PATCH 007/120] [fetcher] getOtherAccount: add time track --- internal/app/fetcher/account.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 7cdb6399..1a6b0428 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -42,6 +42,8 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + defer app.TimeTrack(time.Now(), "getOtherAccount(%s, %d)", a.String(), lastLT) + // first attempt is to look for an account in this given block acc, ok := s.accounts.get(b, a) if ok { From a05ec13ed5fc48d8765e1bbbb7fe22e5fd6a43d4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 18:25:18 +0800 Subject: [PATCH 008/120] [parser] add lru cache for minter addresses --- internal/app/parser/get.go | 56 ++++++++++++++++++++++++++--------- internal/app/parser/parser.go | 7 +++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index c580f381..5d663f03 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -22,6 +22,16 @@ import ( "github.com/tonindexer/anton/internal/core" ) +var ( + dedustFactoryAddr *addr.Address + stonfiRouterAddr *addr.Address +) + +func init() { + dedustFactoryAddr = addr.MustFromBase64("EQBfBWT7X2BHg9tXAxzhz2aKiNTU1tpt5NsiK0uSDW_YAJ67") + stonfiRouterAddr = addr.MustFromBase64("EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt") +} + func getMethodByName(i *core.ContractInterface, n string) *abi.GetMethodDesc { for it := range i.GetMethodsDesc { if i.GetMethodsDesc[it].Name == n { @@ -194,9 +204,17 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta if addr.Equal(itemAddr, &item.Address) { item.Fake = false } + + if !item.Fake { + s.itemsMinterCache.Put(item.Address, minter.Address) + } } func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, idx *big.Int, item *core.AccountState) { + if minterAddr, ok := s.itemsMinterCache.Get(item.Address); ok && addr.Equal(&minter.Address, &minterAddr) { + return + } + desc, err := s.ContractRepo.GetMethodDescription(ctx, known.NFTCollection, "get_nft_address_by_index") if err != nil { panic(fmt.Errorf("get 'get_nft_address_by_index' method description: %w", err)) @@ -207,7 +225,17 @@ func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, s.checkMinter(ctx, minter, item, known.NFTCollection, &desc, args) } -func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountState, ownerAddr *addr.Address, walletAcc *core.AccountState) { +func (s *Service) checkJettonMinter(ctx context.Context, ownerAddr *addr.Address, walletAcc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error)) { + if minterAddr, ok := s.itemsMinterCache.Get(walletAcc.Address); ok && addr.Equal(walletAcc.MinterAddress, &minterAddr) { + return + } + + minter, err := others(ctx, *walletAcc.MinterAddress) + if err != nil { + log.Error().Str("minter_address", walletAcc.MinterAddress.Base64()).Err(err).Msg("get jetton minter state") + return + } + desc, err := s.ContractRepo.GetMethodDescription(ctx, known.JettonMinter, "get_wallet_address") if err != nil { panic(fmt.Errorf("get 'get_wallet_address' method description: %w", err)) @@ -219,10 +247,13 @@ func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountSta } func (s *Service) checkDeDustMinter(ctx context.Context, acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error)) { - factoryAddr := "EQBfBWT7X2BHg9tXAxzhz2aKiNTU1tpt5NsiK0uSDW_YAJ67" - factory, err := others(ctx, *addr.MustFromBase64(factoryAddr)) + if minterAddr, ok := s.itemsMinterCache.Get(acc.Address); ok && addr.Equal(dedustFactoryAddr, &minterAddr) { + return + } + + factory, err := others(ctx, *dedustFactoryAddr) if err != nil { - log.Error().Str("factory_address", factoryAddr).Err(err).Msg("get dedust v2 factory state") + log.Error().Str("factory_address", dedustFactoryAddr.Base64()).Err(err).Msg("get dedust v2 factory state") return } @@ -249,10 +280,13 @@ func (s *Service) checkDeDustMinter(ctx context.Context, acc *core.AccountState, } func (s *Service) checkStonFiMinter(ctx context.Context, acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error)) { - routerAddr := "EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt" - router, err := others(ctx, *addr.MustFromBase64(routerAddr)) + if minterAddr, ok := s.itemsMinterCache.Get(acc.Address); ok && addr.Equal(stonfiRouterAddr, &minterAddr) { + return + } + + router, err := others(ctx, *stonfiRouterAddr) if err != nil { - log.Error().Str("router_address", routerAddr).Err(err).Msg("get stonfi router state") + log.Error().Str("router_address", stonfiRouterAddr.Base64()).Err(err).Msg("get stonfi router state") return } @@ -330,13 +364,7 @@ func (s *Service) callGetMethod( return nil } - minter, err := others(ctx, *acc.MinterAddress) - if err != nil { - log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get jetton minter state") - return nil - } - - s.checkJettonMinter(ctx, minter, acc.OwnerAddress, acc) + s.checkJettonMinter(ctx, acc.OwnerAddress, acc, others) } return nil diff --git a/internal/app/parser/parser.go b/internal/app/parser/parser.go index a9fc5798..0105b9b9 100644 --- a/internal/app/parser/parser.go +++ b/internal/app/parser/parser.go @@ -3,16 +3,22 @@ package parser import ( "encoding/base64" + "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/lru" ) var _ app.ParserService = (*Service)(nil) +const itemsMinterCacheLen = 317750 + type Service struct { *app.ParserConfig accountParseSemaphore chan struct{} + itemsMinterCache *lru.Cache[addr.Address, addr.Address] + bcConfigBase64 string } @@ -21,5 +27,6 @@ func NewService(cfg *app.ParserConfig) *Service { s.ParserConfig = cfg s.bcConfigBase64 = base64.StdEncoding.EncodeToString(cfg.BlockchainConfig.ToBOC()) s.accountParseSemaphore = make(chan struct{}, cfg.MaxAccountParsingWorkers) + s.itemsMinterCache = lru.New[addr.Address, addr.Address](itemsMinterCacheLen) return s } From f55791b6721a6137dd5c877d6259503f7bf3e85f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 19:03:42 +0800 Subject: [PATCH 009/120] [fetcher] getOtherAccount: fetch minter state exactly once --- internal/app/fetcher/account.go | 54 +++++++++++++++++++++++++-------- internal/app/fetcher/fetcher.go | 24 +++++++++++---- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 1a6b0428..c9e9829f 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -2,6 +2,8 @@ package fetcher import ( "context" + "fmt" + "sync" "time" "github.com/pkg/errors" @@ -50,23 +52,49 @@ func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint return acc, nil } - // second attempt is to look for the latest account state in the database - acc, err := s.getLastSeenAccountState(ctx, a, lastLT) - if err == nil { - return acc, nil + itemStateID := core.AccountStateID{Address: a, LastTxLT: lastLT} + + // second attempt is to look into LRU cache, if minter was already fetched for the given id + if m, ok := s.minterStatesCache.Get(itemStateID); ok { + return m, nil } - lvl := log.Warn() - if errors.Is(err, core.ErrNotFound) || errors.Is(err, core.ErrInvalidArg) { - lvl = log.Debug() + + s.minterStatesCacheLocksMx.Lock() + lock, exists := s.minterStatesCacheLocks.Get(itemStateID) + if !exists { + lock = &sync.Once{} + s.minterStatesCacheLocks.Put(itemStateID, lock) } - lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") + s.minterStatesCacheLocksMx.Unlock() + + lock.Do(func() { + // third attempt is to look for the latest account state in the database + acc, err := s.getLastSeenAccountState(ctx, a, lastLT) + if err == nil { + s.minterStatesCache.Put(itemStateID, acc) + return + } + lvl := log.Warn() + if errors.Is(err, core.ErrNotFound) || errors.Is(err, core.ErrInvalidArg) { + lvl = log.Debug() + } + lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") - // third attempt is to get needed contract state from the node - raw, err := s.API.GetAccount(ctx, master, a.MustToTonutils()) - if err != nil { - return nil, errors.Wrapf(err, "cannot get %s account state", a.Base64()) + // forth attempt is to get needed contract state from the node + raw, err := s.API.GetAccount(ctx, master, a.MustToTonutils()) + if err != nil { + log.Error().Err(err).Str("address", a.Base64()).Msg("cannot get account state") + return + } + + s.minterStatesCache.Put(itemStateID, MapAccount(b, raw)) + }) + + if m, ok := s.minterStatesCache.Get(itemStateID); ok { + return m, nil } - return MapAccount(b, raw), nil + + return nil, fmt.Errorf("cannot get account state for (%s, %d)", itemStateID.Address.Base64(), itemStateID.LastTxLT) } return getOtherAccountFunc } diff --git a/internal/app/fetcher/fetcher.go b/internal/app/fetcher/fetcher.go index 06f01e7a..71608f72 100644 --- a/internal/app/fetcher/fetcher.go +++ b/internal/app/fetcher/fetcher.go @@ -1,17 +1,27 @@ package fetcher import ( + "sync" + "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/lru" ) var _ app.FetcherService = (*Service)(nil) +const minterStatesCacheLen = 16384 + type Service struct { *app.FetcherConfig masterWorkchain int32 masterShard uint64 + minterStatesCache *lru.Cache[core.AccountStateID, *core.AccountState] + minterStatesCacheLocks *lru.Cache[core.AccountStateID, *sync.Once] + minterStatesCacheLocksMx sync.Mutex + accounts *accountCache blocks *blocksCache libraries *librariesCache @@ -19,11 +29,13 @@ type Service struct { func NewService(cfg *app.FetcherConfig) *Service { return &Service{ - FetcherConfig: cfg, - masterWorkchain: -1, - masterShard: 0x8000000000000000, - accounts: newAccountCache(), - blocks: newBlocksCache(), - libraries: newLibrariesCache(), + FetcherConfig: cfg, + masterWorkchain: -1, + masterShard: 0x8000000000000000, + minterStatesCache: lru.New[core.AccountStateID, *core.AccountState](minterStatesCacheLen), + minterStatesCacheLocks: lru.New[core.AccountStateID, *sync.Once](minterStatesCacheLen), + accounts: newAccountCache(), + blocks: newBlocksCache(), + libraries: newLibrariesCache(), } } From 7fce15cfc04b8a57c5b083023a56a66b8a7eda53 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 19:14:25 +0800 Subject: [PATCH 010/120] [repo] account filter: always limit number of rows --- internal/core/repository/account/filter.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 43daf8f5..b759358f 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -155,16 +155,10 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts if total < 100000 && f.LatestState { // firstly, select all latest states, then apply limit // https://ottertune.com/blog/how-to-fix-slow-postgresql-queries - rawQuery := "WITH q AS MATERIALIZED (?) SELECT * FROM q" - if f.Limit < total { - rawQuery += fmt.Sprintf(" LIMIT %d", f.Limit) - } + rawQuery := fmt.Sprintf("WITH q AS MATERIALIZED (?) SELECT * FROM q LIMIT %d", f.Limit) err = r.pg.NewRaw(rawQuery, q).Scan(ctx, &ret) } else { - if f.Limit < total { - q = q.Limit(f.Limit) - } - err = q.Scan(ctx) + err = q.Limit(f.Limit).Scan(ctx) } if f.LatestState { From d468ad72a956749b2f5f15d7d9aa29faec6c2bea Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 18 May 2024 20:19:08 +0800 Subject: [PATCH 011/120] [fetcher] getAccount: fetch account state on a block exactly once --- internal/app/fetcher/account.go | 116 +++++++++++++++++++------------- internal/app/fetcher/cache.go | 59 +--------------- internal/app/fetcher/fetcher.go | 29 +++++--- internal/app/fetcher/map.go | 8 ++- internal/app/fetcher/tx.go | 2 +- internal/core/account.go | 7 ++ 6 files changed, 102 insertions(+), 119 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index c9e9829f..4244b3e7 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -42,19 +42,13 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l return accountRes.Rows[0], nil } -func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { +func (s *Service) makeGetOtherAccountFunc(master *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { defer app.TimeTrack(time.Now(), "getOtherAccount(%s, %d)", a.String(), lastLT) - // first attempt is to look for an account in this given block - acc, ok := s.accounts.get(b, a) - if ok { - return acc, nil - } - itemStateID := core.AccountStateID{Address: a, LastTxLT: lastLT} - // second attempt is to look into LRU cache, if minter was already fetched for the given id + // first attempt is to look into LRU cache, if minter was already fetched for the given id if m, ok := s.minterStatesCache.Get(itemStateID); ok { return m, nil } @@ -68,7 +62,7 @@ func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint s.minterStatesCacheLocksMx.Unlock() lock.Do(func() { - // third attempt is to look for the latest account state in the database + // second attempt is to look for the latest account state in the database acc, err := s.getLastSeenAccountState(ctx, a, lastLT) if err == nil { s.minterStatesCache.Put(itemStateID, acc) @@ -80,14 +74,14 @@ func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint } lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") - // forth attempt is to get needed contract state from the node + // third attempt is to get needed contract state from the node raw, err := s.API.GetAccount(ctx, master, a.MustToTonutils()) if err != nil { log.Error().Err(err).Str("address", a.Base64()).Msg("cannot get account state") return } - s.minterStatesCache.Put(itemStateID, MapAccount(b, raw)) + s.minterStatesCache.Put(itemStateID, MapAccount(nil, raw)) }) if m, ok := s.minterStatesCache.Get(itemStateID); ok { @@ -100,61 +94,89 @@ func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint } func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { - acc, ok := s.accounts.get(b, a) - if ok { - return acc, nil - } - if core.SkipAddress(a) { return nil, errors.Wrap(core.ErrNotFound, "skip account") } defer app.TimeTrack(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) - raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) - if err != nil { - return nil, errors.Wrapf(err, "get account") + stateID := core.AccountBlockStateID{Address: a, Workchain: b.Workchain, Shard: b.Shard, BlockSeqNo: b.SeqNo} + + if res, ok := s.accBlockStatesCache.Get(stateID); ok { + return res.acc, res.err + } + + s.accBlockStatesCacheLocksMx.Lock() + lock, exists := s.accBlockStatesCacheLocks.Get(stateID) + if !exists { + lock = &sync.Once{} + s.accBlockStatesCacheLocks.Put(stateID, lock) } + s.accBlockStatesCacheLocksMx.Unlock() - acc = MapAccount(b, raw) + lock.Do(func() { + var ( + acc *core.AccountState + err error + ) + defer func() { s.accBlockStatesCache.Put(stateID, getAccountRes{acc: acc, err: err}) }() - if raw.Code != nil { //nolint:nestif // getting get method hashes from the library - libs, err := s.getAccountLibraries(ctx, raw) + raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) if err != nil { - return nil, errors.Wrapf(err, "get account libraries") - } - if libs != nil { - acc.Libraries = libs.ToBOC() + err = errors.Wrapf(err, "get account") + return } - if raw.Code.GetType() == cell.LibraryCellType { - hash, err := getLibraryHash(raw.Code) - if err != nil { - return nil, errors.Wrap(err, "get library hash") + acc = MapAccount(b, raw) + + if raw.Code != nil { //nolint:nestif // getting get-method hashes from the library + libs, getErr := s.getAccountLibraries(ctx, raw) + if getErr != nil { + err = errors.Wrapf(getErr, "get account libraries") + return + } + if libs != nil { + acc.Libraries = libs.ToBOC() } - lib := s.libraries.get(hash) - if lib != nil && lib.Lib != nil { - acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) + if raw.Code.GetType() == cell.LibraryCellType { + hash, getErr := getLibraryHash(raw.Code) + if getErr != nil { + err = errors.Wrap(getErr, "get library hash") + return + } + + lib := s.libraries.get(hash) + if lib != nil && lib.Lib != nil { + acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) + } + } else { + acc.GetMethodHashes, _ = abi.GetMethodHashes(raw.Code) } - } else { - acc.GetMethodHashes, _ = abi.GetMethodHashes(raw.Code) } - } - if acc.Status == core.NonExist { - return nil, errors.Wrap(core.ErrNotFound, "account does not exists") - } + if acc.Status == core.NonExist { + err = errors.Wrap(core.ErrNotFound, "account does not exists") + return + } + + // sometimes, to parse the full account data we need to get other contracts states + // for example, to get nft item data + getOtherAccount := s.makeGetOtherAccountFunc(master, acc.LastTxLT) + + err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) + if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + err = errors.Wrapf(err, "parse account data (%s)", acc.Address.String()) + return + } - // sometimes, to parse the full account data we need to get other contracts states - // for example, to get nft item data - getOtherAccount := s.makeGetOtherAccountFunc(master, b, acc.LastTxLT) + err = nil + }) - err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) - if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { - return nil, errors.Wrapf(err, "parse account data (%s)", acc.Address.String()) + res, ok := s.accBlockStatesCache.Get(stateID) + if !ok { + panic(fmt.Errorf("cannot get %s parsed account result on (%d, %d, %d)", a.String(), b.Workchain, b.Shard, b.SeqNo)) } - s.accounts.set(b, acc) - return acc, nil + return res.acc, res.err } diff --git a/internal/app/fetcher/cache.go b/internal/app/fetcher/cache.go index 454e4ffb..03adad38 100644 --- a/internal/app/fetcher/cache.go +++ b/internal/app/fetcher/cache.go @@ -6,12 +6,9 @@ import ( "time" "github.com/xssnick/tonutils-go/ton" - - "github.com/tonindexer/anton/addr" - "github.com/tonindexer/anton/internal/core" ) -var cacheInvalidation = time.Minute +var cacheInvalidation = 10 * time.Minute type blocksCache struct { masterBlocks map[uint32]*ton.BlockIDExt @@ -69,60 +66,6 @@ func (c *blocksCache) setShards(master *ton.BlockIDExt, shards []*ton.BlockIDExt c.clearCaches() } -type accountCache struct { - m map[core.BlockID]map[addr.Address]*core.AccountState - lastCleared time.Time - sync.Mutex -} - -func newAccountCache() *accountCache { - return &accountCache{ - m: map[core.BlockID]map[addr.Address]*core.AccountState{}, - lastCleared: time.Time{}, - } -} - -func (c *accountCache) clearCaches() { - if time.Since(c.lastCleared) < cacheInvalidation { - return - } - c.m = map[core.BlockID]map[addr.Address]*core.AccountState{} - c.lastCleared = time.Now() -} - -func (c *accountCache) get(bExt *ton.BlockIDExt, a addr.Address) (*core.AccountState, bool) { - c.Lock() - defer c.Unlock() - - b := core.GetBlockID(bExt) - - m, ok := c.m[b] - if !ok { - return nil, false - } - - acc, ok := m[a] - if !ok { - return nil, false - } - - return acc, true -} - -func (c *accountCache) set(bExt *ton.BlockIDExt, acc *core.AccountState) { - c.Lock() - defer c.Unlock() - - b := core.GetBlockID(bExt) - - if _, ok := c.m[b]; !ok { - c.m[b] = map[addr.Address]*core.AccountState{} - } - - c.m[b][acc.Address] = acc - c.clearCaches() -} - type librariesCache struct { libs map[string]*libDescription sync.Mutex diff --git a/internal/app/fetcher/fetcher.go b/internal/app/fetcher/fetcher.go index 71608f72..c2c7a91d 100644 --- a/internal/app/fetcher/fetcher.go +++ b/internal/app/fetcher/fetcher.go @@ -10,7 +10,12 @@ import ( var _ app.FetcherService = (*Service)(nil) -const minterStatesCacheLen = 16384 +const statesCacheLen = 16384 + +type getAccountRes struct { + acc *core.AccountState + err error +} type Service struct { *app.FetcherConfig @@ -22,20 +27,24 @@ type Service struct { minterStatesCacheLocks *lru.Cache[core.AccountStateID, *sync.Once] minterStatesCacheLocksMx sync.Mutex - accounts *accountCache + accBlockStatesCache *lru.Cache[core.AccountBlockStateID, getAccountRes] + accBlockStatesCacheLocks *lru.Cache[core.AccountBlockStateID, *sync.Once] + accBlockStatesCacheLocksMx sync.Mutex + blocks *blocksCache libraries *librariesCache } func NewService(cfg *app.FetcherConfig) *Service { return &Service{ - FetcherConfig: cfg, - masterWorkchain: -1, - masterShard: 0x8000000000000000, - minterStatesCache: lru.New[core.AccountStateID, *core.AccountState](minterStatesCacheLen), - minterStatesCacheLocks: lru.New[core.AccountStateID, *sync.Once](minterStatesCacheLen), - accounts: newAccountCache(), - blocks: newBlocksCache(), - libraries: newLibrariesCache(), + FetcherConfig: cfg, + masterWorkchain: -1, + masterShard: 0x8000000000000000, + minterStatesCache: lru.New[core.AccountStateID, *core.AccountState](statesCacheLen), + minterStatesCacheLocks: lru.New[core.AccountStateID, *sync.Once](statesCacheLen), + accBlockStatesCache: lru.New[core.AccountBlockStateID, getAccountRes](statesCacheLen), + accBlockStatesCacheLocks: lru.New[core.AccountBlockStateID, *sync.Once](statesCacheLen), + blocks: newBlocksCache(), + libraries: newLibrariesCache(), } } diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index b7b82d42..cb3b1bbe 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -17,9 +17,11 @@ import ( func MapAccount(b *ton.BlockIDExt, acc *tlb.Account) *core.AccountState { ret := new(core.AccountState) - ret.Workchain = b.Workchain - ret.Shard = b.Shard - ret.BlockSeqNo = b.SeqNo + if b != nil { + ret.Workchain = b.Workchain + ret.Shard = b.Shard + ret.BlockSeqNo = b.SeqNo + } ret.IsActive = acc.IsActive ret.Status = core.NonExist diff --git a/internal/app/fetcher/tx.go b/internal/app/fetcher/tx.go index 70da748a..4435e01d 100644 --- a/internal/app/fetcher/tx.go +++ b/internal/app/fetcher/tx.go @@ -63,7 +63,7 @@ func (s *Service) getTransaction(ctx context.Context, master, b *ton.BlockIDExt, if err := accRet.err; err != nil && !errors.Is(err, core.ErrNotFound) { return nil, err } - if accRet.res != nil { + if accRet.err == nil && accRet.res != nil { acc = accRet.res.(*core.AccountState) //nolint:forcetypeassert // that's ok } diff --git a/internal/core/account.go b/internal/core/account.go index f0b3f8d4..92b31215 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -55,6 +55,13 @@ type AccountStateID struct { LastTxLT uint64 } +type AccountBlockStateID struct { + Address addr.Address `ch:"type:String"` + Workchain int32 + Shard int64 + BlockSeqNo uint32 +} + type AccountState struct { ch.CHModel `ch:"account_states,partition:toYYYYMM(updated_at)" json:"-"` bun.BaseModel `bun:"table:account_states" json:"-"` From 8cd2e2b097d8fae18da7738bcc498ebbdcb3fa5d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 07:50:33 +0800 Subject: [PATCH 012/120] [parser] add some time tracking --- internal/app/parser/account.go | 7 +++++++ internal/app/parser/get.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 4bc4e42e..9bd97950 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -3,6 +3,7 @@ package parser import ( "bytes" "context" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -83,6 +84,8 @@ func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { func (s *Service) determineInterfaces(ctx context.Context, acc *core.AccountState) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface + defer app.TimeTrack(time.Now(), "determineInterfaces(%s)", acc.Address.Base64()) + interfaces, err := s.ContractRepo.GetInterfaces(ctx) if err != nil { return nil, errors.Wrap(err, "get contract interfaces") @@ -102,10 +105,14 @@ func (s *Service) ParseAccountData( acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error), ) error { + defer app.TimeTrack(time.Now(), "ParseAccountData(%s)", acc.Address.Base64()) + if s.ContractRepo == nil { return errors.Wrap(app.ErrImpossibleParsing, "no contract repository") } + defer app.TimeTrack(time.Now(), "ParseAccountData[unlocked](%s)", acc.Address.Base64()) + s.accountParseSemaphore <- struct{}{} defer func() { <-s.accountParseSemaphore }() diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 5d663f03..f5c709fa 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "sort" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -376,6 +377,8 @@ func (s *Service) callPossibleGetMethods( others func(context.Context, addr.Address) (*core.AccountState, error), interfaces []*core.ContractInterface, ) { + defer app.TimeTrack(time.Now(), "callPossibleGetMethods(%s, %v)", acc.Address.Base64(), acc.Types) + for _, i := range interfaces { for it := range i.GetMethodsDesc { d := &i.GetMethodsDesc[it] From ee35e25988fd485bc900d7d64aae3c3bdb5a23ce Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 07:56:19 +0800 Subject: [PATCH 013/120] [repo] contract: add mutex for GetInterfaces --- internal/core/repository/contract/contract.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 4509dfff..78c4ac9b 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "strings" + "sync" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -18,6 +19,7 @@ var _ repository.Contract = (*Repository)(nil) type Repository struct { pg *bun.DB cache *cache + mx sync.Mutex } func NewRepository(db *bun.DB) *Repository { @@ -211,6 +213,9 @@ func (r *Repository) GetInterface(ctx context.Context, name abi.ContractName) (* func (r *Repository) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface + r.mx.Lock() + defer r.mx.Unlock() + if i := r.cache.getInterfaces(); i != nil { return i, nil } From 81109e5e281fc9819876af754c6bfa8a08fe02ec Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 08:05:04 +0800 Subject: [PATCH 014/120] [repo] add time track for interfaceMatched --- internal/app/parser/account.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 9bd97950..a1b12f19 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -65,6 +65,8 @@ func matchByGetMethods(acc *core.AccountState, getMethodHashes []int32) bool { } func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { + defer app.TimeTrack(time.Now(), "interfaceMatched(%s, %s)", acc.Address.Base64(), i.Name) + if matchByAddress(acc, i.Addresses) { return true } From ef83dd3b25778c64d963908b23302111571ebb07 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 08:27:40 +0800 Subject: [PATCH 015/120] [repo] contract: fill and cache contract code hash --- internal/app/parser/account.go | 25 +---------- internal/core/contract.go | 1 + internal/core/repository/contract/contract.go | 45 ++++++++++++++++--- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index a1b12f19..87fb7e0a 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -6,8 +6,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" @@ -24,27 +22,6 @@ func matchByAddress(acc *core.AccountState, addresses []*addr.Address) bool { return false } -func matchByCode(acc *core.AccountState, code []byte) bool { - if len(acc.Code) == 0 || len(code) == 0 { - return false - } - - codeCell, err := cell.FromBOC(code) - if err != nil { - log.Error().Err(err).Msg("parse contract interface code") - return false - } - codeHash := codeCell.Hash() - - accCodeCell, err := cell.FromBOC(acc.Code) - if err != nil { - log.Error().Err(err).Str("addr", acc.Address.Base64()).Msg("parse account code cell") - return false - } - - return bytes.Equal(accCodeCell.Hash(), codeHash) -} - func matchByGetMethods(acc *core.AccountState, getMethodHashes []int32) bool { if len(acc.GetMethodHashes) == 0 || len(getMethodHashes) == 0 { return false @@ -71,7 +48,7 @@ func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { return true } - if matchByCode(acc, i.Code) { + if len(acc.Code) != 0 && len(i.Code) != 0 && bytes.Equal(acc.CodeHash, i.CodeHash) { return true } diff --git a/internal/core/contract.go b/internal/core/contract.go index a70a0ea4..9b883907 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -22,6 +22,7 @@ type ContractInterface struct { Name abi.ContractName `bun:",pk" json:"name"` Addresses []*addr.Address `bun:"type:bytea[],unique" json:"addresses,omitempty"` Code []byte `bun:"type:bytea,unique" json:"code,omitempty"` + CodeHash []byte `bun:"-" json:"code_hash,omitempty"` GetMethodsDesc []abi.GetMethodDesc `bun:"type:text" json:"get_methods_descriptors,omitempty"` GetMethodHashes []int32 `bun:"type:integer[]" json:"get_method_hashes,omitempty"` Operations []*ContractOperation `ch:"-" bun:"rel:has-many,join:name=contract_name" json:"operations,omitempty"` diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 78c4ac9b..2033fde9 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -3,12 +3,15 @@ package contract import ( "context" "database/sql" + "fmt" "strings" "sync" "github.com/pkg/errors" "github.com/uptrace/bun" + "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository" @@ -17,13 +20,14 @@ import ( var _ repository.Contract = (*Repository)(nil) type Repository struct { - pg *bun.DB - cache *cache - mx sync.Mutex + pg *bun.DB + cache *cache + codeHashMap map[string][]byte + mx sync.RWMutex } func NewRepository(db *bun.DB) *Repository { - return &Repository{pg: db, cache: newCache()} + return &Repository{pg: db, codeHashMap: map[string][]byte{}, cache: newCache()} } func CreateTables(ctx context.Context, pgDB *bun.DB) error { @@ -193,6 +197,30 @@ func (r *Repository) DeleteInterface(ctx context.Context, name abi.ContractName) return nil } +func (r *Repository) setContractCodeHash(i *core.ContractInterface) { + if len(i.Code) == 0 { + return + } + + r.mx.RLock() + i.CodeHash = r.codeHashMap[string(i.Code)] + r.mx.RUnlock() + + if i.CodeHash != nil { + return + } + + codeCell, err := cell.FromBOC(i.Code) + if err != nil { + panic(fmt.Errorf("parse contract interface code of %s interface", i.Name)) + } + i.CodeHash = codeCell.Hash() + + r.mx.Lock() + r.codeHashMap[string(i.Code)] = i.CodeHash + r.mx.Unlock() +} + func (r *Repository) GetInterface(ctx context.Context, name abi.ContractName) (*core.ContractInterface, error) { var ret core.ContractInterface @@ -207,15 +235,14 @@ func (r *Repository) GetInterface(ctx context.Context, name abi.ContractName) (* return nil, err } + r.setContractCodeHash(&ret) + return &ret, nil } func (r *Repository) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface - r.mx.Lock() - defer r.mx.Unlock() - if i := r.cache.getInterfaces(); i != nil { return i, nil } @@ -225,6 +252,10 @@ func (r *Repository) GetInterfaces(ctx context.Context) ([]*core.ContractInterfa return nil, err } + for _, i := range ret { + r.setContractCodeHash(i) + } + r.cache.setInterfaces(ret) return ret, nil From ca8d59c50f5bb76b97707546c78bd6fc8ae8f4b0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 08:38:21 +0800 Subject: [PATCH 016/120] [parser] emulateGetMethod: add time track --- internal/app/parser/get.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index f5c709fa..7bbe9d6c 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -59,6 +59,8 @@ func (s *Service) emulateGetMethod(ctx context.Context, d *abi.GetMethodDesc, ac }) } + defer app.TimeTrack(time.Now(), fmt.Sprintf("emulateGetMethod(%s, %s)", acc.Address.Base64(), d.Name)) + codeBase64, dataBase64, librariesBase64 := base64.StdEncoding.EncodeToString(acc.Code), base64.StdEncoding.EncodeToString(acc.Data), From abbe03fbee07e6113ca6993ee7aa81db01fa5a0f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 20:22:38 +0800 Subject: [PATCH 017/120] [repo] account: add timer for getCodeData --- internal/app/fetcher.go | 11 ----------- internal/app/fetcher/account.go | 6 +++--- internal/app/fetcher/tx.go | 5 ++--- internal/app/indexer/fetch.go | 3 +-- internal/app/indexer/save.go | 8 ++++---- internal/app/parser/account.go | 8 +++----- internal/app/parser/get.go | 4 ++-- internal/app/rescan/account.go | 2 +- internal/core/repository/account/filter.go | 3 +++ internal/core/timer.go | 16 ++++++++++++++++ 10 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 internal/core/timer.go diff --git a/internal/app/fetcher.go b/internal/app/fetcher.go index f4ddadf7..dd9bb69f 100644 --- a/internal/app/fetcher.go +++ b/internal/app/fetcher.go @@ -2,10 +2,7 @@ package app import ( "context" - "fmt" - "time" - "github.com/rs/zerolog/log" "github.com/xssnick/tonutils-go/ton" "github.com/tonindexer/anton/internal/core" @@ -20,14 +17,6 @@ type FetcherConfig struct { Parser ParserService } -func TimeTrack(start time.Time, fun string, args ...any) { - elapsed := float64(time.Since(start)) / 1e9 - if elapsed < 0.1 { - return - } - log.Debug().Str("func", fmt.Sprintf(fun, args...)).Float64("elapsed", elapsed).Msg("timer") -} - type FetcherService interface { LookupMaster(ctx context.Context, api ton.APIClientWrapped, seqNo uint32) (*ton.BlockIDExt, error) UnseenBlocks(ctx context.Context, masterSeqNo uint32) (master *ton.BlockIDExt, shards []*ton.BlockIDExt, err error) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 4244b3e7..d3aa9a8f 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -19,7 +19,7 @@ import ( ) func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%s, %d)", a.String(), lastLT) + defer core.Timer(time.Now(), "getLastSeenAccountState(%s, %d)", a.String(), lastLT) lastLT++ @@ -44,7 +44,7 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l func (s *Service) makeGetOtherAccountFunc(master *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getOtherAccount(%s, %d)", a.String(), lastLT) + defer core.Timer(time.Now(), "getOtherAccount(%s, %d)", a.String(), lastLT) itemStateID := core.AccountStateID{Address: a, LastTxLT: lastLT} @@ -98,7 +98,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a return nil, errors.Wrap(core.ErrNotFound, "skip account") } - defer app.TimeTrack(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) + defer core.Timer(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) stateID := core.AccountBlockStateID{Address: a, Workchain: b.Workchain, Shard: b.Shard, BlockSeqNo: b.SeqNo} diff --git a/internal/app/fetcher/tx.go b/internal/app/fetcher/tx.go index 4435e01d..b372d9d0 100644 --- a/internal/app/fetcher/tx.go +++ b/internal/app/fetcher/tx.go @@ -10,7 +10,6 @@ import ( "github.com/xssnick/tonutils-go/ton" "github.com/tonindexer/anton/addr" - "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" ) @@ -96,7 +95,7 @@ func (s *Service) getTransactions(ctx context.Context, master, b *ton.BlockIDExt err error } - defer app.TimeTrack(time.Now(), "getTransactions(%d, %d)", b.Workchain, b.SeqNo) + defer core.Timer(time.Now(), "getTransactions(%d, %d)", b.Workchain, b.SeqNo) ch := make(chan ret, len(ids)) @@ -142,7 +141,7 @@ func (s *Service) BlockTransactions(ctx context.Context, master, b *ton.BlockIDE err error ) - defer app.TimeTrack(time.Now(), "BlockTransactions(%d, %d)", b.Workchain, b.SeqNo) + defer core.Timer(time.Now(), "BlockTransactions(%d, %d)", b.Workchain, b.SeqNo) for more { fetchedIDs, more, err = s.fetchTxIDs(ctx, b, after) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index bf773b51..d4235415 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -11,7 +11,6 @@ import ( "github.com/rs/zerolog/log" "github.com/xssnick/tonutils-go/ton" - "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" ) @@ -43,7 +42,7 @@ func (s *Service) fetchMaster(seq uint32) *core.Block { err error } - defer app.TimeTrack(time.Now(), "fetchMaster(%d)", seq) + defer core.Timer(time.Now(), "fetchMaster(%d)", seq) for { ctx := context.Background() diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index f2fb730b..95d0adc7 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -47,14 +47,14 @@ func (s *Service) insertData( } if err := func() error { - defer app.TimeTrack(time.Now(), "AddAccountStates(%d)", len(acc)) + defer core.Timer(time.Now(), "AddAccountStates(%d)", len(acc)) return s.accountRepo.AddAccountStates(ctx, dbTx, acc) }(); err != nil { return errors.Wrap(err, "add account states") } if err := func() error { - defer app.TimeTrack(time.Now(), "AddMessages(%d)", len(msg)) + defer core.Timer(time.Now(), "AddMessages(%d)", len(msg)) sort.Slice(msg, func(i, j int) bool { return msg[i].CreatedLT < msg[j].CreatedLT }) return s.msgRepo.AddMessages(ctx, dbTx, msg) }(); err != nil { @@ -62,14 +62,14 @@ func (s *Service) insertData( } if err := func() error { - defer app.TimeTrack(time.Now(), "AddTransactions(%d)", len(tx)) + defer core.Timer(time.Now(), "AddTransactions(%d)", len(tx)) return s.txRepo.AddTransactions(ctx, dbTx, tx) }(); err != nil { return errors.Wrap(err, "add transactions") } if err := func() error { - defer app.TimeTrack(time.Now(), "AddBlocks(%d)", len(b)) + defer core.Timer(time.Now(), "AddBlocks(%d)", len(b)) return s.blockRepo.AddBlocks(ctx, dbTx, b) }(); err != nil { return errors.Wrap(err, "add blocks") diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 87fb7e0a..29184992 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -42,7 +42,7 @@ func matchByGetMethods(acc *core.AccountState, getMethodHashes []int32) bool { } func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { - defer app.TimeTrack(time.Now(), "interfaceMatched(%s, %s)", acc.Address.Base64(), i.Name) + defer core.Timer(time.Now(), "interfaceMatched(%s, %s)", acc.Address.Base64(), i.Name) if matchByAddress(acc, i.Addresses) { return true @@ -63,7 +63,7 @@ func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { func (s *Service) determineInterfaces(ctx context.Context, acc *core.AccountState) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface - defer app.TimeTrack(time.Now(), "determineInterfaces(%s)", acc.Address.Base64()) + defer core.Timer(time.Now(), "determineInterfaces(%s)", acc.Address.Base64()) interfaces, err := s.ContractRepo.GetInterfaces(ctx) if err != nil { @@ -84,13 +84,11 @@ func (s *Service) ParseAccountData( acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error), ) error { - defer app.TimeTrack(time.Now(), "ParseAccountData(%s)", acc.Address.Base64()) - if s.ContractRepo == nil { return errors.Wrap(app.ErrImpossibleParsing, "no contract repository") } - defer app.TimeTrack(time.Now(), "ParseAccountData[unlocked](%s)", acc.Address.Base64()) + defer core.Timer(time.Now(), "ParseAccountData(%s)", acc.Address.Base64()) s.accountParseSemaphore <- struct{}{} defer func() { <-s.accountParseSemaphore }() diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 7bbe9d6c..58fbb9b4 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -59,7 +59,7 @@ func (s *Service) emulateGetMethod(ctx context.Context, d *abi.GetMethodDesc, ac }) } - defer app.TimeTrack(time.Now(), fmt.Sprintf("emulateGetMethod(%s, %s)", acc.Address.Base64(), d.Name)) + defer core.Timer(time.Now(), fmt.Sprintf("emulateGetMethod(%s, %s)", acc.Address.Base64(), d.Name)) codeBase64, dataBase64, librariesBase64 := base64.StdEncoding.EncodeToString(acc.Code), @@ -379,7 +379,7 @@ func (s *Service) callPossibleGetMethods( others func(context.Context, addr.Address) (*core.AccountState, error), interfaces []*core.ContractInterface, ) { - defer app.TimeTrack(time.Now(), "callPossibleGetMethods(%s, %v)", acc.Address.Base64(), acc.Types) + defer core.Timer(time.Now(), "callPossibleGetMethods(%s, %v)", acc.Address.Base64(), acc.Types) for _, i := range interfaces { for it := range i.GetMethodsDesc { diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 0c050d4a..46a20bee 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -16,7 +16,7 @@ import ( ) func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getRecentAccountState(%s, %d)", a.String(), lastLT) + defer core.Timer(time.Now(), "getRecentAccountState(%s, %d)", a.String(), lastLT) if minter, ok := s.minterStateCache.get(a, lastLT); ok { return minter, nil diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index b759358f..5e64c30d 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -221,6 +222,8 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR } func (r *Repository) getCodeData(ctx context.Context, rows []*core.AccountState, excludeCode, excludeData bool) error { //nolint:gocognit,gocyclo // TODO: make one function working for both code and data + defer core.Timer(time.Now(), "getCodeData(%d, %t, %t)", len(rows), excludeCode, excludeData) + codeHashesSet, dataHashesSet := map[string]struct{}{}, map[string]struct{}{} for _, row := range rows { if !excludeCode && len(row.Code) == 0 && len(row.CodeHash) == 32 { diff --git a/internal/core/timer.go b/internal/core/timer.go new file mode 100644 index 00000000..787e1d73 --- /dev/null +++ b/internal/core/timer.go @@ -0,0 +1,16 @@ +package core + +import ( + "fmt" + "time" + + "github.com/rs/zerolog/log" +) + +func Timer(start time.Time, fun string, args ...any) { + elapsed := float64(time.Since(start)) / 1e9 + if elapsed < 0.1 { + return + } + log.Debug().Str("func", fmt.Sprintf(fun, args...)).Float64("elapsed", elapsed).Msg("timer") +} From fa19cfc80a8b67a32b7ce5057471d1fb84922117 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 22:38:33 +0800 Subject: [PATCH 018/120] [indexer] getMessageSource: fix check for masterchain messages --- internal/app/indexer/save.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 95d0adc7..786fc786 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -143,7 +143,7 @@ func (s *Service) getMessageSource(ctx context.Context, msg *core.Message) (skip } // some masterchain messages does not have source - if msg.SrcAddress.Workchain() == -1 || msg.DstAddress.Workchain() == -1 { + if msg.SrcAddress.Workchain() == -1 && msg.DstAddress.Workchain() == -1 { return false } From 497785e0992cb395eee6b569c93a3e5de192cfeb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 22:42:04 +0800 Subject: [PATCH 019/120] [indexer] fetch and save blocks simultaneously --- internal/app/indexer/fetch.go | 62 ++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index d4235415..0263bbf7 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -2,7 +2,6 @@ package indexer import ( "context" - "sort" "strings" "sync" "time" @@ -137,49 +136,60 @@ func (s *Service) fetchMaster(seq uint32) *core.Block { } } -func (s *Service) fetchMastersConcurrent(fromBlock uint32) []*core.Block { - var blocks []*core.Block - var wg sync.WaitGroup +func publishProcessedBlocks(fromBlock uint32, processed []*core.Block, results chan<- *core.Block) (uint32, []*core.Block) { + for { + var found bool + + for it, b := range processed { + if b.SeqNo == fromBlock { + continue + } + + results <- b + + fromBlock++ + + copy(processed[:it], processed[it+1:]) + processed = processed[:len(processed)-1] + + found = true + + break + } + + if !found { + break + } + } + + return fromBlock, processed +} - wg.Add(s.Workers) +func (s *Service) fetchMastersConcurrent(fromBlock uint32, results chan<- *core.Block) (nextBlock uint32) { + var blocks []*core.Block ch := make(chan *core.Block, s.Workers) + defer close(ch) for i := 0; i < s.Workers; i++ { go func(seq uint32) { - defer wg.Done() ch <- s.fetchMaster(seq) }(fromBlock + uint32(i)) } - wg.Wait() - close(ch) - - for b := range ch { - if b == nil { - continue - } + for i := 0; i < s.Workers; i++ { + b := <-ch blocks = append(blocks, b) + fromBlock, blocks = publishProcessedBlocks(fromBlock, blocks, results) } - sort.Slice(blocks, func(i, j int) bool { - return blocks[i].SeqNo < blocks[j].SeqNo - }) - - return blocks + return fromBlock } func (s *Service) fetchMasterLoop(fromBlock uint32, results chan<- *core.Block) { defer s.wg.Done() for s.running() { - blocks := s.fetchMastersConcurrent(fromBlock) - for i := range blocks { - if fromBlock != blocks[i].SeqNo { - break - } - results <- blocks[i] - fromBlock++ - } + fromBlock = s.fetchMastersConcurrent(fromBlock, results) } } From 605033a9bca12d5cc6fd5d25edaeb9275a4d2577 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 19 May 2024 22:55:28 +0800 Subject: [PATCH 020/120] [indexer] publishProcessedBlocks: fix typo --- internal/app/indexer/fetch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index 0263bbf7..44bd3c79 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -141,7 +141,7 @@ func publishProcessedBlocks(fromBlock uint32, processed []*core.Block, results c var found bool for it, b := range processed { - if b.SeqNo == fromBlock { + if b.SeqNo != fromBlock { continue } From ea6c3acd97088f9ca50bd871a68e55cf72eb41a4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 21 May 2024 16:31:44 +0800 Subject: [PATCH 021/120] [fetcher] getAccount: rewrite account cache in case of error --- internal/app/fetcher/account.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index d3aa9a8f..f9ed5f2a 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -102,13 +102,14 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a stateID := core.AccountBlockStateID{Address: a, Workchain: b.Workchain, Shard: b.Shard, BlockSeqNo: b.SeqNo} - if res, ok := s.accBlockStatesCache.Get(stateID); ok { - return res.acc, res.err + res, ok := s.accBlockStatesCache.Get(stateID) + if ok && res.err == nil { + return res.acc, nil } s.accBlockStatesCacheLocksMx.Lock() lock, exists := s.accBlockStatesCacheLocks.Get(stateID) - if !exists { + if !exists || res.err != nil { lock = &sync.Once{} s.accBlockStatesCacheLocks.Put(stateID, lock) } @@ -173,7 +174,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a err = nil }) - res, ok := s.accBlockStatesCache.Get(stateID) + res, ok = s.accBlockStatesCache.Get(stateID) if !ok { panic(fmt.Errorf("cannot get %s parsed account result on (%d, %d, %d)", a.String(), b.Workchain, b.Shard, b.SeqNo)) } From b1dedec43b9aa7972dc2a9573a77ba450aad2359 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 29 May 2024 13:44:36 +0800 Subject: [PATCH 022/120] [indexer] fetchMastersConcurrent: catch up with maximum number of workers, continue with just one --- internal/app/indexer/fetch.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index 44bd3c79..04a934c9 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -168,6 +168,21 @@ func publishProcessedBlocks(fromBlock uint32, processed []*core.Block, results c func (s *Service) fetchMastersConcurrent(fromBlock uint32, results chan<- *core.Block) (nextBlock uint32) { var blocks []*core.Block + m, err := s.API.GetMasterchainInfo(context.Background()) + if err != nil { + log.Error().Err(err).Msg("get masterchain info") + time.Sleep(100 * time.Millisecond) + return fromBlock + } + + workers := s.Workers + if diff := int(m.SeqNo) - int(fromBlock) + 1; diff < workers { + workers = diff + } + if workers <= 0 { // should never be triggered + workers = 1 + } + ch := make(chan *core.Block, s.Workers) defer close(ch) From c452f5b1eece5a8fde42a5af8c8f78fbf6585779 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 30 May 2024 19:04:44 +0800 Subject: [PATCH 023/120] [fetcher] getAccount: start timer after cache get --- internal/app/fetcher/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index f9ed5f2a..a5b737d9 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -98,8 +98,6 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a return nil, errors.Wrap(core.ErrNotFound, "skip account") } - defer core.Timer(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) - stateID := core.AccountBlockStateID{Address: a, Workchain: b.Workchain, Shard: b.Shard, BlockSeqNo: b.SeqNo} res, ok := s.accBlockStatesCache.Get(stateID) @@ -116,6 +114,8 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a s.accBlockStatesCacheLocksMx.Unlock() lock.Do(func() { + defer core.Timer(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) + var ( acc *core.AccountState err error From a189a17f5c903ab6087ef6487900b0cbc5899b6b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 30 May 2024 19:05:01 +0800 Subject: [PATCH 024/120] [fetcher] getAccountLibraries: add timer --- internal/app/fetcher/account.go | 2 +- internal/app/fetcher/libraries.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index a5b737d9..ac203b71 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -131,7 +131,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a acc = MapAccount(b, raw) if raw.Code != nil { //nolint:nestif // getting get-method hashes from the library - libs, getErr := s.getAccountLibraries(ctx, raw) + libs, getErr := s.getAccountLibraries(ctx, a, raw) if getErr != nil { err = errors.Wrapf(getErr, "get account libraries") return diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index 6b274d81..a7d69d3e 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -2,10 +2,14 @@ package fetcher import ( "context" + "time" "github.com/pkg/errors" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/core" ) type libDescription struct { @@ -57,7 +61,9 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { return hashes, nil } -func (s *Service) getAccountLibraries(ctx context.Context, raw *tlb.Account) (*cell.Cell, error) { +func (s *Service) getAccountLibraries(ctx context.Context, a addr.Address, raw *tlb.Account) (*cell.Cell, error) { + defer core.Timer(time.Now(), "getAccountLibraries(%s)", a.String()) + hashes, err := findLibraries(raw.Code) if err != nil { return nil, errors.Wrapf(err, "find libraries") From 69969b522ec4794137d1fac2f0b984a30b842510 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 19 Jun 2024 10:10:25 +0300 Subject: [PATCH 025/120] [repo] tx: do not ch count rows on filter by hash --- internal/core/repository/tx/filter.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/core/repository/tx/filter.go b/internal/core/repository/tx/filter.go index 5196ef27..d1ada867 100644 --- a/internal/core/repository/tx/filter.go +++ b/internal/core/repository/tx/filter.go @@ -108,13 +108,18 @@ func (r *Repository) FilterTransactions(ctx context.Context, req *filter.Transac if err != nil { return res, err } - if len(res.Rows) == 0 { - return res, nil - } - res.Total, err = r.countTx(ctx, req) - if err != nil { - return res, err + switch { + case len(res.Rows) == 0: + + case len(req.Hash) > 0: + res.Total = len(res.Rows) + + default: + res.Total, err = r.countTx(ctx, req) + if err != nil { + return res, err + } } return res, nil From 18fb161da699c864e54753b3b2db017618427180 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 21 Jun 2024 18:09:47 +0300 Subject: [PATCH 026/120] SkipAddress: add quackquack account states --- internal/core/account.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index 92b31215..a5748d98 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -159,6 +159,9 @@ func SkipAddress(a addr.Address) bool { return true case "EQAWBIxrfQDExJSfFmE5UL1r9drse0dQx_eaV8w9S77VK32F": // tongo emulator segmentation fault return true + case "EQCnBscEi-KGfqJ5Wk6R83yrqtmUum94SXnSDz3AOQfHGjDw", + "EQA9xJgsYbsTjWxEcaxv8DLW3iRJtHzjwFzFAEWVxup0WH0R": // quackquack (?) + return true default: return false } From d47585beff7603231f545e23838ab773511fbdad Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 21 Jun 2024 18:10:29 +0300 Subject: [PATCH 027/120] [fetcher] fix tests --- internal/app/fetcher/libraries_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/fetcher/libraries_test.go b/internal/app/fetcher/libraries_test.go index 2a5fb987..f2e6d22a 100644 --- a/internal/app/fetcher/libraries_test.go +++ b/internal/app/fetcher/libraries_test.go @@ -35,7 +35,7 @@ func TestService_getAccountLibraries(t *testing.T) { raw, err := s.API.GetAccount(ctx, m, a.MustToTonutils()) require.NoError(t, err) - _, err = s.getAccountLibraries(ctx, raw) + _, err = s.getAccountLibraries(ctx, *a, raw) require.NoError(t, err) } } @@ -57,7 +57,7 @@ func TestService_getAccountLibraries_emulate(t *testing.T) { acc := MapAccount(m, raw) - lib, err := s.getAccountLibraries(ctx, raw) + lib, err := s.getAccountLibraries(ctx, *a, raw) require.NoError(t, err) acc.Libraries = lib.ToBOC() From 2806da5d51a4efdb810dc6a64ca5f752b7847664 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 16:24:12 +0300 Subject: [PATCH 028/120] [cmd] add fillMissedClickHouseData command --- cmd/db/db.go | 111 ++++++++++++++++++++++++ internal/core/block.go | 1 + internal/core/repository/block/block.go | 36 ++++++++ 3 files changed, 148 insertions(+) diff --git a/cmd/db/db.go b/cmd/db/db.go index 4c9d1170..9e05500d 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -16,7 +16,9 @@ import ( "github.com/uptrace/go-clickhouse/chmigrate" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" "github.com/tonindexer/anton/internal/core/repository" + "github.com/tonindexer/anton/internal/core/repository/block" "github.com/tonindexer/anton/migrations/chmigrations" "github.com/tonindexer/anton/migrations/pgmigrations" @@ -467,5 +469,114 @@ var Command = &cli.Command{ } }, }, + { + Name: "fillMissedClickHouseData", + Usage: "Transfers missed blocks from PostgreSQL to ClickHouse", + Action: func(c *cli.Context) error { + chURL := env.GetString("DB_CH_URL", "") + pgURL := env.GetString("DB_PG_URL", "") + + conn, err := repository.ConnectDB(c.Context, chURL, pgURL) + if err != nil { + return errors.Wrap(err, "cannot connect to the databases") + } + defer conn.Close() + + blockRepo := block.NewRepository(conn.CH, conn.PG) + + blockIds, err := blockRepo.GetMissedMasterBlocks(c.Context) + if err != nil { + return errors.Wrap(err, "get missed masterchain blocks") + } + if len(blockIds) == 0 { + return errors.Wrap(core.ErrNotFound, "could not find any missed blocks") + } + + m, err := blockRepo.GetLastMasterBlock(c.Context) + if err != nil { + return errors.Wrap(err, "get last master block") + } + + for _, b := range blockIds { + res, err := blockRepo.FilterBlocks(c.Context, &filter.BlocksReq{ + Workchain: &m.Workchain, + Shard: &m.Shard, + SeqNo: &b, + WithShards: true, + WithAccountStates: true, + WithTransactions: true, + WithTransactionMessages: true, + }) + if err != nil { + return errors.Wrapf(err, "filter blocks on (%d, %x, %d)", m.Workchain, m.Shard, m.SeqNo) + } + + var ( + masterBlocks []*core.Block + shardBlocks []*core.Block + transactions []*core.Transaction + messages []*core.Message + accounts []*core.AccountState + ) + + for _, row := range res.Rows { + masterBlocks = append(masterBlocks, row) + + shardBlocks = append(shardBlocks, row.Shards...) + + accounts = append(accounts, row.Accounts...) + transactions = append(transactions, row.Transactions...) + for _, tx := range row.Transactions { + messages = append(messages, tx.InMsg) + messages = append(messages, tx.OutMsg...) + } + + for _, shard := range row.Shards { + accounts = append(accounts, shard.Accounts...) + transactions = append(transactions, shard.Transactions...) + for _, tx := range shard.Transactions { + transactions = append(transactions, tx) + messages = append(messages, tx.InMsg) + messages = append(messages, tx.OutMsg...) + } + } + } + + log.Info(). + Int32("workchain", m.Workchain). + Int64("shard", m.Shard). + Uint32("seq_no", b). + Int("master_blocks_len", len(masterBlocks)). + Int("shard_blocks_len", len(shardBlocks)). + Int("transactions_len", len(transactions)). + Int("messages_len", len(messages)). + Int("account_states_len", len(accounts)). + Msg("insert new missed block") + + _, err = conn.CH.NewInsert().Model(&accounts).Exec(c.Context) + if err != nil { + return err + } + _, err = conn.CH.NewInsert().Model(&messages).Exec(c.Context) + if err != nil { + return err + } + _, err = conn.CH.NewInsert().Model(&transactions).Exec(c.Context) + if err != nil { + return err + } + _, err = conn.CH.NewInsert().Model(&shardBlocks).Exec(c.Context) + if err != nil { + return err + } + _, err = conn.CH.NewInsert().Model(&masterBlocks).Exec(c.Context) + if err != nil { + return err + } + } + + return nil + }, + }, }, } diff --git a/internal/core/block.go b/internal/core/block.go index e44e5409..61d5fb1f 100644 --- a/internal/core/block.go +++ b/internal/core/block.go @@ -58,4 +58,5 @@ type BlockRepository interface { AddBlocks(ctx context.Context, tx bun.Tx, info []*Block) error GetLastMasterBlock(ctx context.Context) (*Block, error) CountMasterBlocks(ctx context.Context) (int, error) + GetMissedMasterBlocks(ctx context.Context) ([]uint32, error) } diff --git a/internal/core/repository/block/block.go b/internal/core/repository/block/block.go index e04b7f61..729c88e3 100644 --- a/internal/core/repository/block/block.go +++ b/internal/core/repository/block/block.go @@ -111,3 +111,39 @@ func (r *Repository) CountMasterBlocks(ctx context.Context) (int, error) { } return ret, nil } + +func (r *Repository) GetMissedMasterBlocks(ctx context.Context) (res []uint32, err error) { + var ret []struct { + SeqNo uint32 + NextSeqNo uint32 + } + + err = r.ch.NewSelect(). + TableExpr("(?) as sq", + r.ch.NewSelect().Model((*core.Block)(nil)). + ColumnExpr("seq_no"). + ColumnExpr("any(seq_no) over (order by seq_no asc rows between 1 following and 1 following) as next_seq_no"). + Where("workchain = -1"). + Order("seq_no asc"), + ). + Where("seq_no != next_seq_no - 1"). + Where("next_seq_no != 0"). + Order("seq_no asc"). + Scan(ctx, &ret) + if err != nil { + return nil, err + } + + var lastMissedBlock uint32 + for _, r := range ret { + for i := r.SeqNo - 10; i < r.NextSeqNo+10; i++ { + if i <= lastMissedBlock { + continue + } + lastMissedBlock = i + res = append(res, i) + } + } + + return res, nil +} From 4e8b3e3e41c87e0a79abb79292eec9325a3e92a0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 16:37:46 +0300 Subject: [PATCH 029/120] [cmd] fillMissedClickHouseData: check data length on insert --- cmd/db/db.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index 9e05500d..c7ebcb35 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -553,25 +553,30 @@ var Command = &cli.Command{ Int("account_states_len", len(accounts)). Msg("insert new missed block") - _, err = conn.CH.NewInsert().Model(&accounts).Exec(c.Context) - if err != nil { - return err + if len(accounts) > 0 { + if _, err := conn.CH.NewInsert().Model(&accounts).Exec(c.Context); err != nil { + return err + } } - _, err = conn.CH.NewInsert().Model(&messages).Exec(c.Context) - if err != nil { - return err + if len(messages) > 0 { + if _, err := conn.CH.NewInsert().Model(&messages).Exec(c.Context); err != nil { + return err + } } - _, err = conn.CH.NewInsert().Model(&transactions).Exec(c.Context) - if err != nil { - return err + if len(transactions) > 0 { + if _, err := conn.CH.NewInsert().Model(&transactions).Exec(c.Context); err != nil { + return err + } } - _, err = conn.CH.NewInsert().Model(&shardBlocks).Exec(c.Context) - if err != nil { - return err + if len(shardBlocks) > 0 { + if _, err := conn.CH.NewInsert().Model(&shardBlocks).Exec(c.Context); err != nil { + return err + } } - _, err = conn.CH.NewInsert().Model(&masterBlocks).Exec(c.Context) - if err != nil { - return err + if len(masterBlocks) > 0 { + if _, err := conn.CH.NewInsert().Model(&masterBlocks).Exec(c.Context); err != nil { + return err + } } } From b61cc483cdb34ddc5f3b73a589bf01bc90afc302 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 16:40:48 +0300 Subject: [PATCH 030/120] [cmd] fillMissedClickHouseData: check incoming message is not nil --- cmd/db/db.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index c7ebcb35..8243825c 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -527,7 +527,9 @@ var Command = &cli.Command{ accounts = append(accounts, row.Accounts...) transactions = append(transactions, row.Transactions...) for _, tx := range row.Transactions { - messages = append(messages, tx.InMsg) + if tx.InMsg != nil { + messages = append(messages, tx.InMsg) + } messages = append(messages, tx.OutMsg...) } @@ -536,7 +538,9 @@ var Command = &cli.Command{ transactions = append(transactions, shard.Transactions...) for _, tx := range shard.Transactions { transactions = append(transactions, tx) - messages = append(messages, tx.InMsg) + if tx.InMsg != nil { + messages = append(messages, tx.InMsg) + } messages = append(messages, tx.OutMsg...) } } From 5ddd62f9e55458bd7696ef46d041a395376cf9e9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:22:57 +0300 Subject: [PATCH 031/120] [cmd] fillMissedClickHouseData: fix implicit memory aliasing --- cmd/db/db.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index 8243825c..b549da0f 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -497,11 +497,11 @@ var Command = &cli.Command{ return errors.Wrap(err, "get last master block") } - for _, b := range blockIds { + for i := range blockIds { res, err := blockRepo.FilterBlocks(c.Context, &filter.BlocksReq{ Workchain: &m.Workchain, Shard: &m.Shard, - SeqNo: &b, + SeqNo: &blockIds[i], WithShards: true, WithAccountStates: true, WithTransactions: true, @@ -549,7 +549,7 @@ var Command = &cli.Command{ log.Info(). Int32("workchain", m.Workchain). Int64("shard", m.Shard). - Uint32("seq_no", b). + Uint32("seq_no", blockIds[i]). Int("master_blocks_len", len(masterBlocks)). Int("shard_blocks_len", len(shardBlocks)). Int("transactions_len", len(transactions)). From 6e75921a7a40a2eaa9d91d0cd138366d2a143c7e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:23:55 +0300 Subject: [PATCH 032/120] [indexer] fetchMastersConcurrent: fix ineffectual assignment to workers --- internal/app/indexer/fetch.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index 04a934c9..6e8ca21f 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -183,16 +183,16 @@ func (s *Service) fetchMastersConcurrent(fromBlock uint32, results chan<- *core. workers = 1 } - ch := make(chan *core.Block, s.Workers) + ch := make(chan *core.Block, workers) defer close(ch) - for i := 0; i < s.Workers; i++ { + for i := 0; i < workers; i++ { go func(seq uint32) { ch <- s.fetchMaster(seq) }(fromBlock + uint32(i)) } - for i := 0; i < s.Workers; i++ { + for i := 0; i < workers; i++ { b := <-ch blocks = append(blocks, b) fromBlock, blocks = publishProcessedBlocks(fromBlock, blocks, results) From e3f3a67f114f702bab5ec32aa198840911a10996 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:32:47 +0300 Subject: [PATCH 033/120] [repo] contract: fix interface tests --- internal/core/repository/contract/contract_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 14357875..b7ca73ed 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -3,6 +3,7 @@ package contract_test import ( "context" "database/sql" + "encoding/base64" "encoding/json" "strings" "testing" @@ -12,6 +13,7 @@ import ( "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" + "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/abi/known" @@ -92,10 +94,16 @@ func TestRepository_AddContracts(t *testing.T) { err := json.Unmarshal(definitionSchema, &d.Schema) require.Nil(t, err) + codeBoC, err := base64.StdEncoding.DecodeString("te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8GBwgJAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNCgsCASAMDQBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgBwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AAAK9ADJ7VQAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABgCKUASBAQj0WTDtRNCBAUDXIMgBzxb0AMntVAFysI4jghBkc3Rygx6xcIAYUAXLBVADzxYj+gITy2rLH8s/yYBA+wCSXwPiAgEgDg8AWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAIBWBARABG4yX7UTQ1wsfgAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGACASASEwAZrc52omhAIGuQ64X/wAAZrx32omhAEGuQ64WPwGb/qfE=") + require.NoError(t, err) + codeCell, err := cell.FromBOC(codeBoC) + require.NoError(t, err) + i := &core.ContractInterface{ Name: known.NFTItem, Addresses: []*addr.Address{rndm.Address()}, - Code: rndm.Bytes(128), + Code: codeBoC, + CodeHash: codeCell.Hash(), GetMethodsDesc: []abi.GetMethodDesc{ { Name: "get_nft_content", From 5adfa4765b7f515e62cc368fd0c2409915b689f4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:35:40 +0300 Subject: [PATCH 034/120] [repo] do not count total number of rows by default --- internal/app/fetcher/account.go | 1 - internal/app/query/query.go | 4 +++- internal/core/filter/account.go | 5 ++--- internal/core/filter/block.go | 3 ++- internal/core/filter/msg.go | 3 ++- internal/core/filter/tx.go | 1 + internal/core/repository/account/filter.go | 22 +++++++++---------- .../core/repository/account/filter_test.go | 18 +++++++-------- internal/core/repository/block/filter.go | 8 ++++--- internal/core/repository/block/filter_test.go | 6 ++--- internal/core/repository/msg/filter.go | 17 +++++++++----- internal/core/repository/msg/filter_test.go | 8 +++---- internal/core/repository/repository_test.go | 4 ++++ internal/core/repository/tx/filter.go | 2 +- internal/core/repository/tx/filter_test.go | 8 ++++--- 15 files changed, 63 insertions(+), 47 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index ac203b71..6b6ccfa7 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -28,7 +28,6 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l Addresses: []*addr.Address{&a}, Order: "DESC", AfterTxLT: &lastLT, - NoCount: true, Limit: 1, } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index e3d0c84b..efa63bb7 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -136,7 +136,9 @@ func (s *Service) fetchSkippedAccounts(ctx context.Context, req *filter.Accounts return errors.Wrap(err, "get address label") } - res.Total += 1 + if req.Count { + res.Total += 1 + } res.Rows = append(res.Rows, parsed) } diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index 91d6032c..cc531374 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -41,16 +41,15 @@ type AccountsReq struct { ExcludeColumn []string // TODO: support relations - NoCount bool - Order string `form:"order"` // ASC, DESC AfterTxLT *uint64 `form:"after"` Limit int `form:"limit"` + Count bool `form:"count"` } type AccountsRes struct { - Total int `json:"total"` + Total int `json:"total,omitempty"` Rows []*core.AccountState `json:"results"` } diff --git a/internal/core/filter/block.go b/internal/core/filter/block.go index 9600a988..51e39244 100644 --- a/internal/core/filter/block.go +++ b/internal/core/filter/block.go @@ -24,10 +24,11 @@ type BlocksReq struct { AfterSeqNo *uint32 `form:"after"` Limit int `form:"limit"` + Count bool `form:"count"` } type BlocksRes struct { - Total int `json:"total"` + Total int `json:"total,omitempty"` Rows []*core.Block `json:"results"` } diff --git a/internal/core/filter/msg.go b/internal/core/filter/msg.go index acc1fed3..1b722841 100644 --- a/internal/core/filter/msg.go +++ b/internal/core/filter/msg.go @@ -28,10 +28,11 @@ type MessagesReq struct { AfterTxLT *uint64 `form:"after"` Limit int `form:"limit"` + Count bool `form:"count"` } type MessagesRes struct { - Total int `json:"total"` + Total int `json:"total,omitempty"` Rows []*core.Message `json:"results"` } diff --git a/internal/core/filter/tx.go b/internal/core/filter/tx.go index ac770fe0..48ec9a5f 100644 --- a/internal/core/filter/tx.go +++ b/internal/core/filter/tx.go @@ -28,6 +28,7 @@ type TransactionsReq struct { AfterTxLT *uint64 `form:"after"` Limit int `form:"limit"` + Count bool `form:"count"` } type TransactionsRes struct { diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 5e64c30d..c6f9da33 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -84,7 +84,7 @@ func flattenStateIDs(ids []*core.AccountStateID) (ret [][]any) { return } -func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { //nolint:gocyclo,gocognit // that's ok +func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq) (ret []*core.AccountState, err error) { //nolint:gocyclo,gocognit // that's ok var ( q *bun.SelectQuery prefix, statesTable string @@ -153,14 +153,14 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts q = q.Order(statesTable + orderBy + " " + strings.ToUpper(f.Order)) } - if total < 100000 && f.LatestState { - // firstly, select all latest states, then apply limit - // https://ottertune.com/blog/how-to-fix-slow-postgresql-queries - rawQuery := fmt.Sprintf("WITH q AS MATERIALIZED (?) SELECT * FROM q LIMIT %d", f.Limit) - err = r.pg.NewRaw(rawQuery, q).Scan(ctx, &ret) - } else { - err = q.Limit(f.Limit).Scan(ctx) - } + // if total < 100000 && f.LatestState { + // // firstly, select all latest states, then apply limit + // // https://ottertune.com/blog/how-to-fix-slow-postgresql-queries + // rawQuery := fmt.Sprintf("WITH q AS MATERIALIZED (?) SELECT * FROM q LIMIT %d", f.Limit) + // err = r.pg.NewRaw(rawQuery, q).Scan(ctx, &ret) + // } else { + err = q.Limit(f.Limit).Scan(ctx) + // } if f.LatestState { for _, a := range latest { @@ -301,7 +301,7 @@ func (r *Repository) FilterAccounts(ctx context.Context, f *filter.AccountsReq) f.Limit = 3 } - if !f.NoCount { + if f.Count { res.Total, err = r.countAccountStates(ctx, f) if err != nil && !errors.Is(err, core.ErrNotImplemented) { return res, errors.Wrap(err, "count account states") @@ -311,7 +311,7 @@ func (r *Repository) FilterAccounts(ctx context.Context, f *filter.AccountsReq) } } - res.Rows, err = r.filterAccountStates(ctx, f, res.Total) + res.Rows, err = r.filterAccountStates(ctx, f) if err != nil { return res, err } diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index 11645552..f658abca 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -193,7 +193,7 @@ func TestRepository_FilterAccounts(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, Addresses: []*addr.Address{address}, - Order: "ASC", Limit: len(addressStates), + Order: "ASC", Limit: len(addressStates), Count: true, }) require.Nil(t, err) require.Equal(t, 15, results.Total) @@ -208,7 +208,7 @@ func TestRepository_FilterAccounts(t *testing.T) { WithCodeData: true, Addresses: []*addr.Address{&latest.Address}, LatestState: true, - ExcludeColumn: []string{"code"}, + ExcludeColumn: []string{"code"}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -223,7 +223,7 @@ func TestRepository_FilterAccounts(t *testing.T) { WithCodeData: true, Addresses: []*addr.Address{&latest.Address}, LatestState: true, - ExcludeColumn: []string{"code"}, + ExcludeColumn: []string{"code"}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -235,7 +235,7 @@ func TestRepository_FilterAccounts(t *testing.T) { WithCodeData: true, ContractTypes: []abi.ContractName{"special", "some_nonsense"}, LatestState: true, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 15, results.Total) @@ -246,7 +246,7 @@ func TestRepository_FilterAccounts(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, MinterAddress: latestState.MinterAddress, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 5, results.Total) @@ -257,7 +257,7 @@ func TestRepository_FilterAccounts(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, OwnerAddress: latestState.OwnerAddress, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -269,7 +269,7 @@ func TestRepository_FilterAccounts(t *testing.T) { WithCodeData: true, LatestState: true, OwnerAddress: latestState.OwnerAddress, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -280,7 +280,7 @@ func TestRepository_FilterAccounts(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, StateIDs: []*core.AccountStateID{{Address: latestState.Address, LastTxLT: latestState.LastTxLT}}, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 0, results.Total) @@ -366,7 +366,7 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { WithCodeData: true, ContractTypes: []abi.ContractName{"special"}, LatestState: true, - Order: "DESC", Limit: 1, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index 5f1871f5..638821c6 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -186,9 +186,11 @@ func (r *Repository) FilterBlocks(ctx context.Context, f *filter.BlocksReq) (*fi return res, nil } - res.Total, err = r.countBlocks(ctx, f) - if err != nil { - return res, err + if f.Count { + res.Total, err = r.countBlocks(ctx, f) + if err != nil { + return res, err + } } return res, nil diff --git a/internal/core/repository/block/filter_test.go b/internal/core/repository/block/filter_test.go index 4b36cd6c..7159f7be 100644 --- a/internal/core/repository/block/filter_test.go +++ b/internal/core/repository/block/filter_test.go @@ -82,7 +82,7 @@ func TestRepository_FilterBlocks(t *testing.T) { // Shard: &shard.Shard, // SeqNo: &shard.SeqNo, - AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, + AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 100, res.Total) @@ -94,7 +94,7 @@ func TestRepository_FilterBlocks(t *testing.T) { Workchain: &shard.Workchain, SeqNo: &shard.SeqNo, - AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, + AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -107,7 +107,7 @@ func TestRepository_FilterBlocks(t *testing.T) { WithShards: true, - AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, + AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 2dd43e01..938ab1e0 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -113,13 +113,18 @@ func (r *Repository) FilterMessages(ctx context.Context, req *filter.MessagesReq if err != nil { return res, err } - if len(res.Rows) == 0 { - return res, nil - } - res.Total, err = r.countMsg(ctx, req) - if err != nil { - return res, err + switch { + case len(res.Rows) == 0: + + case len(req.Hash) > 0: + res.Total = len(res.Rows) + + case req.Count: + res.Total, err = r.countMsg(ctx, req) + if err != nil { + return res, err + } } return res, nil diff --git a/internal/core/repository/msg/filter_test.go b/internal/core/repository/msg/filter_test.go index 61244225..6fc9de76 100644 --- a/internal/core/repository/msg/filter_test.go +++ b/internal/core/repository/msg/filter_test.go @@ -50,7 +50,7 @@ func TestRepository_FilterMessages(t *testing.T) { expected := *messages[0] res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - Hash: messages[0].Hash, + Hash: messages[0].Hash, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -62,7 +62,7 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by address", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - DstAddresses: []*addr.Address{&messages[0].DstAddress}, + DstAddresses: []*addr.Address{&messages[0].DstAddress}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -71,7 +71,7 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by contract", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - DstContracts: []string{"special"}, + DstContracts: []string{"special"}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -83,7 +83,7 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by operation name", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - OperationNames: []string{"special_op"}, + OperationNames: []string{"special_op"}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index 44d5582b..377771b6 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -207,6 +207,7 @@ func TestRelations(t *testing.T) { res, err := accountRepo.FilterAccounts(ctx, &filter.AccountsReq{ Addresses: addresses, LatestState: true, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -216,6 +217,7 @@ func TestRelations(t *testing.T) { t.Run("get messages with payloads", func(t *testing.T) { res, err := msgRepo.FilterMessages(ctx, &filter.MessagesReq{ DstAddresses: addresses, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -232,6 +234,7 @@ func TestRelations(t *testing.T) { Addresses: addresses, WithAccountState: true, WithMessages: true, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -250,6 +253,7 @@ func TestRelations(t *testing.T) { WithTransactions: true, WithTransactionAccountState: true, WithTransactionMessages: true, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) diff --git a/internal/core/repository/tx/filter.go b/internal/core/repository/tx/filter.go index d1ada867..eb0aadf9 100644 --- a/internal/core/repository/tx/filter.go +++ b/internal/core/repository/tx/filter.go @@ -115,7 +115,7 @@ func (r *Repository) FilterTransactions(ctx context.Context, req *filter.Transac case len(req.Hash) > 0: res.Total = len(res.Rows) - default: + case req.Count: res.Total, err = r.countTx(ctx, req) if err != nil { return res, err diff --git a/internal/core/repository/tx/filter_test.go b/internal/core/repository/tx/filter_test.go index 59d4134a..f72ca7c0 100644 --- a/internal/core/repository/tx/filter_test.go +++ b/internal/core/repository/tx/filter_test.go @@ -42,7 +42,7 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - Hash: transactions[0].Hash, + Hash: transactions[0].Hash, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -51,7 +51,7 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by incoming message hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - InMsgHash: transactions[0].InMsgHash, + InMsgHash: transactions[0].InMsgHash, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -60,7 +60,7 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by addresses", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - Addresses: []*addr.Address{&transactions[0].Address}, + Addresses: []*addr.Address{&transactions[0].Address}, Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -74,6 +74,7 @@ func TestRepository_FilterTransactions(t *testing.T) { Shard: transactions[0].Shard, SeqNo: transactions[0].BlockSeqNo, }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -85,6 +86,7 @@ func TestRepository_FilterTransactions(t *testing.T) { Workchain: new(int32), Order: "ASC", Limit: len(transactions), + Count: true, }) require.Nil(t, err) require.Equal(t, len(transactions), res.Total) From c4998fb34fb0edc66cb4fc464e38d44d05e362e4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:40:59 +0300 Subject: [PATCH 035/120] [api] do not count total number of rows by default --- api/http/docs.go | 34 +++++++++++++++++++++++++++++++++ api/http/swagger.json | 34 +++++++++++++++++++++++++++++++++ api/http/swagger.yaml | 24 +++++++++++++++++++++++ internal/api/http/controller.go | 12 ++++++++---- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index 9834ee0b..c36d7e58 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -96,6 +96,13 @@ const docTemplate = `{ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -293,6 +300,13 @@ const docTemplate = `{ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -558,6 +572,13 @@ const docTemplate = `{ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -853,6 +874,13 @@ const docTemplate = `{ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -1598,6 +1626,12 @@ const docTemplate = `{ "type": "integer" } }, + "code_hash": { + "type": "array", + "items": { + "type": "integer" + } + }, "get_method_hashes": { "type": "array", "items": { diff --git a/api/http/swagger.json b/api/http/swagger.json index 241504d8..1e4c28bb 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -93,6 +93,13 @@ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -290,6 +297,13 @@ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -555,6 +569,13 @@ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -850,6 +871,13 @@ "description": "limit", "name": "limit", "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "count total number of rows", + "name": "count", + "in": "query" } ], "responses": { @@ -1595,6 +1623,12 @@ "type": "integer" } }, + "code_hash": { + "type": "array", + "items": { + "type": "integer" + } + }, "get_method_hashes": { "type": "array", "items": { diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 56118394..6da58895 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -439,6 +439,10 @@ definitions: items: type: integer type: array + code_hash: + items: + type: integer + type: array get_method_hashes: items: type: integer @@ -813,6 +817,11 @@ paths: maximum: 10000 name: limit type: integer + - default: false + description: count total number of rows + in: query + name: count + type: boolean produces: - application/json responses: @@ -947,6 +956,11 @@ paths: maximum: 100 name: limit type: integer + - default: false + description: count total number of rows + in: query + name: count + type: boolean produces: - application/json responses: @@ -1123,6 +1137,11 @@ paths: maximum: 10000 name: limit type: integer + - default: false + description: count total number of rows + in: query + name: count + type: boolean produces: - application/json responses: @@ -1323,6 +1342,11 @@ paths: maximum: 10000 name: limit type: integer + - default: false + description: count total number of rows + in: query + name: count + type: boolean produces: - application/json responses: diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 63e84cff..bd0bcf30 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -222,13 +222,14 @@ func (c *Controller) GetDefinitions(ctx *gin.Context) { // @Tags block // @Accept json // @Produce json -// @Param workchain query int false "workchain" default(-1) +// @Param workchain query int false "workchain" default(-1) // @Param shard query int64 false "shard" // @Param seq_no query int false "seq_no" -// @Param with_transactions query bool false "include transactions" default(false) -// @Param order query string false "order by seq_no" Enums(ASC, DESC) default(DESC) +// @Param with_transactions query bool false "include transactions" default(false) +// @Param order query string false "order by seq_no" Enums(ASC, DESC) default(DESC) // @Param after query int false "start from this seq_no" -// @Param limit query int false "limit" default(3) maximum(100) +// @Param limit query int false "limit" default(3) maximum(100) +// @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.BlocksRes // @Router /blocks [get] func (c *Controller) GetBlocks(ctx *gin.Context) { @@ -344,6 +345,7 @@ func (c *Controller) GetLabels(ctx *gin.Context) { // @Param order query string false "order by last_tx_lt" Enums(ASC, DESC) default(DESC) // @Param after query int false "start from this last_tx_lt" // @Param limit query int false "limit" default(3) maximum(10000) +// @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.AccountsRes // @Router /accounts [get] func (c *Controller) GetAccounts(ctx *gin.Context) { @@ -490,6 +492,7 @@ func (c *Controller) AggregateAccountsHistory(ctx *gin.Context) { // @Param order query string false "order by created_lt" Enums(ASC, DESC) default(DESC) // @Param after query int false "start from this created_lt" // @Param limit query int false "limit" default(3) maximum(10000) +// @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.TransactionsRes // @Router /transactions [get] func (c *Controller) GetTransactions(ctx *gin.Context) { @@ -597,6 +600,7 @@ func (c *Controller) AggregateTransactionsHistory(ctx *gin.Context) { // @Param order query string false "order by created_lt" Enums(ASC, DESC) default(DESC) // @Param after query int false "start from this created_lt" // @Param limit query int false "limit" default(3) maximum(10000) +// @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.MessagesRes // @Router /messages [get] func (c *Controller) GetMessages(ctx *gin.Context) { From f1ac9c19b80cc770fffd591aabf4b8f74d322e01 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 24 Jul 2024 17:49:12 +0300 Subject: [PATCH 036/120] [api] transactions filter: total rows omitempty --- internal/core/filter/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/filter/tx.go b/internal/core/filter/tx.go index 48ec9a5f..d40055d7 100644 --- a/internal/core/filter/tx.go +++ b/internal/core/filter/tx.go @@ -32,7 +32,7 @@ type TransactionsReq struct { } type TransactionsRes struct { - Total int `json:"total"` + Total int `json:"total,omitempty"` Rows []*core.Transaction `json:"results"` } From 647aef07e3d93a10527a64924c49870f26afe863 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 4 Aug 2024 12:48:07 +0300 Subject: [PATCH 037/120] [cmd] db: add subcommand to fill missed raw account states code and data --- cmd/db/db.go | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/cmd/db/db.go b/cmd/db/db.go index b549da0f..fd2ae501 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -11,6 +11,8 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/ton" "github.com/uptrace/bun/migrate" "github.com/uptrace/go-clickhouse/chmigrate" @@ -587,5 +589,147 @@ var Command = &cli.Command{ return nil }, }, + { + Name: "fillMissedRawAccountStates", + Usage: "Fetches missed raw account states code and data", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "limit", + Value: 10000, + Usage: "batch size for update", + }, + &cli.Uint64Flag{ + Name: "start-from", + Value: 0, + Usage: "last tx lt to start from", + }, + }, + Action: func(ctx *cli.Context) error { + chURL := env.GetString("DB_CH_URL", "") + pgURL := env.GetString("DB_PG_URL", "") + + conn, err := repository.ConnectDB(ctx.Context, chURL, pgURL) + if err != nil { + return errors.Wrap(err, "cannot connect to the databases") + } + defer conn.Close() + + client := liteclient.NewConnectionPool() + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() + for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { + split := strings.Split(addr, "|") + if len(split) != 2 { + return fmt.Errorf("wrong server address format '%s'", addr) + } + host, key := split[0], split[1] + if err := client.AddConnection(ctx.Context, host, key); err != nil { + return errors.Wrapf(err, "cannot add connection with %s host and %s key", host, key) + } + } + + fetchAccountCodeData := func(s *core.AccountState) error { + block := core.Block{Workchain: s.Workchain, Shard: s.Shard, SeqNo: s.BlockSeqNo} + + err := conn.PG.NewSelect().Model(&block). + Where("workchain = ?workchain"). + Where("shard = ?shard"). + Where("seq_no = ?seq_no"). + Scan(ctx.Context) + if err != nil { + return errors.Wrap(err, "select block") + } + + rawBlock := ton.BlockIDExt{ + Workchain: block.Workchain, Shard: block.Shard, SeqNo: block.SeqNo, + RootHash: block.RootHash, FileHash: block.FileHash, + } + + acc, err := api.GetAccount(ctx.Context, &rawBlock, s.Address.MustToTonutils()) + if err != nil { + return errors.Wrap(err, "get raw account") + } + + if acc.Code == nil { + return fmt.Errorf("account state has no code") + } + if acc.Data == nil { + return fmt.Errorf("account state has no data") + } + + code := core.AccountStateCode{ + CodeHash: acc.Code.Hash(), + Code: acc.Code.ToBOC(), + } + if _, err := conn.CH.NewInsert().Model(&code).Exec(ctx.Context); err != nil { + return errors.Wrapf(err, "write code to key-value store") + } + + data := core.AccountStateData{ + DataHash: acc.Data.Hash(), + Data: acc.Data.ToBOC(), + } + if _, err := conn.CH.NewInsert().Model(&data).Exec(ctx.Context); err != nil { + return errors.Wrapf(err, "write data to key-value store") + } + + return nil + } + + latestLT := ctx.Uint64("start-from") + batch := ctx.Int("limit") + _main: + for { + var states []*core.AccountState + + err := conn.PG.NewSelect().Model(&states). + Where("last_tx_lt > ?", latestLT). + Order("ASC"). + Limit(batch). + Scan(ctx.Context) + if err != nil { + return errors.Wrapf(err, "scan account states from %d", latestLT) + } + if len(states) == 0 { + log.Info().Msg("no states left") + return nil + } + + var maxTxLt uint64 + for _, s := range states { + if s.LastTxLT > maxTxLt { + maxTxLt = s.LastTxLT + } + + var code core.AccountStateCode + err := conn.CH.NewSelect().Model(&code).Where("code_hash = ?", s.CodeHash).Scan(ctx.Context) + if err != nil { + log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account code") + if err := fetchAccountCodeData(s); err != nil { + log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") + continue _main + } + continue + } + + var data core.AccountStateData + err = conn.CH.NewSelect().Model(&data).Where("data_hash = ?", s.DataHash).Scan(ctx.Context) + if err != nil { + log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account data") + if err := fetchAccountCodeData(s); err != nil { + log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") + continue _main + } + continue + } + } + + for _, s := range states { + if s.LastTxLT > latestLT && s.LastTxLT != maxTxLt { + latestLT = s.LastTxLT + } + } + } + }, + }, }, } From 742d2d7f7eecc6954a89169694fed91f21bee2c0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 4 Aug 2024 12:51:29 +0300 Subject: [PATCH 038/120] [cmd] db.fillMissedRawAccountStates: fix typo on account states select --- cmd/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index fd2ae501..43a886a6 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -683,7 +683,7 @@ var Command = &cli.Command{ err := conn.PG.NewSelect().Model(&states). Where("last_tx_lt > ?", latestLT). - Order("ASC"). + Order("last_tx_lt ASC"). Limit(batch). Scan(ctx.Context) if err != nil { From b86c03e7b14676892185adbd20fb4a3fb656d3a4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 4 Aug 2024 12:54:30 +0300 Subject: [PATCH 039/120] [cmd] db.fillMissedRawAccountStates: check code and data hashes are not empty --- cmd/db/db.go | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index 43a886a6..9c99ae61 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -700,26 +700,30 @@ var Command = &cli.Command{ maxTxLt = s.LastTxLT } - var code core.AccountStateCode - err := conn.CH.NewSelect().Model(&code).Where("code_hash = ?", s.CodeHash).Scan(ctx.Context) - if err != nil { - log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account code") - if err := fetchAccountCodeData(s); err != nil { - log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") - continue _main + if len(s.CodeHash) > 0 { + var code core.AccountStateCode + err := conn.CH.NewSelect().Model(&code).Where("code_hash = ?", s.CodeHash).Scan(ctx.Context) + if err != nil { + log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account code") + if err := fetchAccountCodeData(s); err != nil { + log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") + continue _main + } + continue } - continue } - var data core.AccountStateData - err = conn.CH.NewSelect().Model(&data).Where("data_hash = ?", s.DataHash).Scan(ctx.Context) - if err != nil { - log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account data") - if err := fetchAccountCodeData(s); err != nil { - log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") - continue _main + if len(s.DataHash) > 0 { + var data core.AccountStateData + err = conn.CH.NewSelect().Model(&data).Where("data_hash = ?", s.DataHash).Scan(ctx.Context) + if err != nil { + log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account data") + if err := fetchAccountCodeData(s); err != nil { + log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") + continue _main + } + continue } - continue } } From 955e58ce7a0773349b956174c0e21ad660536ff7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 4 Aug 2024 12:59:30 +0300 Subject: [PATCH 040/120] [cmd] db.fillMissedRawAccountStates: add log for number of account states checked --- cmd/db/db.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/db/db.go b/cmd/db/db.go index 9c99ae61..a8a289c1 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -677,6 +677,7 @@ var Command = &cli.Command{ latestLT := ctx.Uint64("start-from") batch := ctx.Int("limit") + totalChecked := 0 _main: for { var states []*core.AccountState @@ -732,6 +733,11 @@ var Command = &cli.Command{ latestLT = s.LastTxLT } } + + totalChecked += len(states) + if totalChecked%500000 == 0 { + log.Info().Int("total_checked", totalChecked).Uint64("last_tx_lt", latestLT).Msg("checkpoint") + } } }, }, From f37ca0fa4fe8ec4dab358ef33802118e9af8e17b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 4 Aug 2024 13:08:09 +0300 Subject: [PATCH 041/120] [cmd] db.fillMissedRawAccountStates: get code and data in batches --- cmd/db/db.go | 121 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 33 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index a8a289c1..c1f09354 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -10,6 +10,7 @@ import ( "github.com/allisson/go-env" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/uptrace/go-clickhouse/ch" "github.com/urfave/cli/v2" "github.com/xssnick/tonutils-go/liteclient" "github.com/xssnick/tonutils-go/ton" @@ -675,10 +676,91 @@ var Command = &cli.Command{ return nil } + getCodeData := func(ctx context.Context, rows []*core.AccountState) error { //nolint:gocognit,gocyclo // TODO: make one function working for both code and data + codeHashesSet, dataHashesSet := map[string]struct{}{}, map[string]struct{}{} + for _, row := range rows { + if len(row.CodeHash) == 32 { + codeHashesSet[string(row.CodeHash)] = struct{}{} + } + if len(row.DataHash) == 32 { + dataHashesSet[string(row.DataHash)] = struct{}{} + } + } + + batchLen := 1000 + codeHashBatches, dataHashBatches := make([][][]byte, 1), make([][][]byte, 1) + appendHash := func(hash []byte, batches [][][]byte) [][][]byte { + b := batches[len(batches)-1] + if len(b) >= batchLen { + b = [][]byte{} + batches = append(batches, b) + } + batches[len(batches)-1] = append(b, hash) + return batches + } + for h := range codeHashesSet { + codeHashBatches = appendHash([]byte(h), codeHashBatches) + } + for h := range dataHashesSet { + dataHashBatches = appendHash([]byte(h), dataHashBatches) + } + + codeRes, dataRes := map[string][]byte{}, map[string][]byte{} + for _, b := range codeHashBatches { + var codeArr []*core.AccountStateCode + err := conn.CH.NewSelect().Model(&codeArr).Where("code_hash IN ?", ch.In(b)).Scan(ctx) + if err != nil { + return errors.Wrapf(err, "get code") + } + for _, x := range codeArr { + codeRes[string(x.CodeHash)] = x.Code + } + } + for _, b := range dataHashBatches { + var dataArr []*core.AccountStateData + err := conn.CH.NewSelect().Model(&dataArr).Where("data_hash IN ?", ch.In(b)).Scan(ctx) + if err != nil { + return errors.Wrapf(err, "get data") + } + for _, x := range dataArr { + dataRes[string(x.DataHash)] = x.Data + } + } + + for _, row := range rows { + var ok bool + if len(row.CodeHash) == 32 { + if row.Code, ok = codeRes[string(row.CodeHash)]; !ok { + log.Warn(). + Str("address", row.Address.String()). + Uint64("last_tx_lt", row.LastTxLT). + Msg("missed account code") + if err := fetchAccountCodeData(row); err != nil { + return errors.Wrapf(err, "(%s, %d)", row.Address.String(), row.LastTxLT) + } + continue + } + } + if len(row.DataHash) == 32 { + if row.Data, ok = dataRes[string(row.DataHash)]; !ok { + log.Warn(). + Str("address", row.Address.String()). + Uint64("last_tx_lt", row.LastTxLT). + Msg("missed account data") + if err := fetchAccountCodeData(row); err != nil { + return errors.Wrapf(err, "(%s, %d)", row.Address.String(), row.LastTxLT) + } + continue + } + } + } + + return nil + } + latestLT := ctx.Uint64("start-from") batch := ctx.Int("limit") totalChecked := 0 - _main: for { var states []*core.AccountState @@ -695,44 +777,17 @@ var Command = &cli.Command{ return nil } - var maxTxLt uint64 - for _, s := range states { - if s.LastTxLT > maxTxLt { - maxTxLt = s.LastTxLT - } - - if len(s.CodeHash) > 0 { - var code core.AccountStateCode - err := conn.CH.NewSelect().Model(&code).Where("code_hash = ?", s.CodeHash).Scan(ctx.Context) - if err != nil { - log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account code") - if err := fetchAccountCodeData(s); err != nil { - log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") - continue _main - } - continue - } - } - - if len(s.DataHash) > 0 { - var data core.AccountStateData - err = conn.CH.NewSelect().Model(&data).Where("data_hash = ?", s.DataHash).Scan(ctx.Context) - if err != nil { - log.Warn().Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("missed account data") - if err := fetchAccountCodeData(s); err != nil { - log.Error().Err(err).Str("address", s.Address.String()).Uint64("last_tx_lt", s.LastTxLT).Msg("renew account code and data") - continue _main - } - continue - } - } + if err := getCodeData(ctx.Context, states); err != nil { + log.Error().Err(err).Msg("fill code data") + continue } for _, s := range states { - if s.LastTxLT > latestLT && s.LastTxLT != maxTxLt { + if s.LastTxLT > latestLT { latestLT = s.LastTxLT } } + latestLT-- totalChecked += len(states) if totalChecked%500000 == 0 { From 5447988e2458d61a574fb2bd94d569a0e729c5e7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 5 Aug 2024 12:56:28 +0300 Subject: [PATCH 042/120] [cmd] db.fillMissedRawAccountStates: stop when number of scanned accounts is less than a batch length --- cmd/db/db.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index c1f09354..8c8bb51a 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -772,16 +772,17 @@ var Command = &cli.Command{ if err != nil { return errors.Wrapf(err, "scan account states from %d", latestLT) } - if len(states) == 0 { - log.Info().Msg("no states left") - return nil - } if err := getCodeData(ctx.Context, states); err != nil { log.Error().Err(err).Msg("fill code data") continue } + if len(states) < batch { + log.Info().Msg("no states left") + return nil + } + for _, s := range states { if s.LastTxLT > latestLT { latestLT = s.LastTxLT From 2014dcd5aee45f7b5a77aeafaf073e83b4f56089 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 23 Aug 2024 13:18:27 +0300 Subject: [PATCH 043/120] SkipAddress: add some mainnet addresses --- internal/core/account.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index a5748d98..d37ae0f0 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -162,6 +162,12 @@ func SkipAddress(a addr.Address) bool { case "EQCnBscEi-KGfqJ5Wk6R83yrqtmUum94SXnSDz3AOQfHGjDw", "EQA9xJgsYbsTjWxEcaxv8DLW3iRJtHzjwFzFAEWVxup0WH0R": // quackquack (?) return true + case "EQCqNjAPkigLdS5gxHiHitWuzF3ZN-gX7MlX4Qfy2cGS3FWx": // ton-squid + return true + case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7": + return true + case "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK": + return true default: return false } From 4b241b00bb4037ea22aaa01a675047476a921cd3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 23 Aug 2024 13:25:27 +0300 Subject: [PATCH 044/120] SkipAddress: add heavy mainnet highload v2 wallet --- internal/core/account.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index d37ae0f0..f50acdd7 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -164,6 +164,8 @@ func SkipAddress(a addr.Address) bool { return true case "EQCqNjAPkigLdS5gxHiHitWuzF3ZN-gX7MlX4Qfy2cGS3FWx": // ton-squid return true + case "EQCp6qUScSUYB66ExDIlla8kfnUpP5cLZ_zhy4nlOPC-fqFo": // highload wallet v2 with heavy data + return true case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7": return true case "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK": From dfd9a4d1a9c869bf69f90ed604ed143d599fbece Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 23 Aug 2024 13:31:26 +0300 Subject: [PATCH 045/120] SkipAddress: add heavy mainnet nft collection and jetton distribution address --- internal/core/account.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index f50acdd7..21d9fde6 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -166,6 +166,10 @@ func SkipAddress(a addr.Address) bool { return true case "EQCp6qUScSUYB66ExDIlla8kfnUpP5cLZ_zhy4nlOPC-fqFo": // highload wallet v2 with heavy data return true + case "EQCTsnUmD2wvN-SBaa7CMF1sgTfC-YNywqbdPepKw34VBglS": // TryTON NFT collection + return true + case "EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh": // TON lama distribution + return true case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7": return true case "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK": From 2c2825625e5d8215414bee4fdfb58ebfab39cf78 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 23 Aug 2024 13:56:05 +0300 Subject: [PATCH 046/120] SkipAddress: add eth token bridge collector and some other unknown heavy accounts --- internal/core/account.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index 21d9fde6..928b1ee7 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -170,9 +170,13 @@ func SkipAddress(a addr.Address) bool { return true case "EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh": // TON lama distribution return true - case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7": + case "EQDF6fj6ydJJX_ArwxINjP-0H8zx982W4XgbkKzGvceUWvXl": // ETH Token Bridge Collector return true - case "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK": + case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7", + "EQAlMRLTYOoG6kM0d3dLHqgK30ol3qIYwMNtEelktzXP_pD5", + "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK", + "EQAU35_2hAbymisgUrhGa4bIJUtEJjVNVS7zBrqfKaENd67N", + "EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ": return true default: return false From 5c0c6caefcd50dd37b80080554a4f73bee1f51fb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 23 Aug 2024 14:31:15 +0300 Subject: [PATCH 047/120] SkipAddress: one more jetton token distribution address --- internal/core/account.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index 928b1ee7..74234787 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -166,9 +166,13 @@ func SkipAddress(a addr.Address) bool { return true case "EQCp6qUScSUYB66ExDIlla8kfnUpP5cLZ_zhy4nlOPC-fqFo": // highload wallet v2 with heavy data return true + case "EQC1Bq1GJY9ON_2WpSroVlXpejzfLNA8XoL2MYxtN50ZbJfN": // TryTON + return true case "EQCTsnUmD2wvN-SBaa7CMF1sgTfC-YNywqbdPepKw34VBglS": // TryTON NFT collection return true - case "EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh": // TON lama distribution + case "EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh": // TON lama jetton distribution + return true + case "EQBvc1QLuqTMx0NNTZ4DD__UzfTvkEOJMs67XoZhHVihWtMN": // POO jetton distribution return true case "EQDF6fj6ydJJX_ArwxINjP-0H8zx982W4XgbkKzGvceUWvXl": // ETH Token Bridge Collector return true @@ -176,7 +180,9 @@ func SkipAddress(a addr.Address) bool { "EQAlMRLTYOoG6kM0d3dLHqgK30ol3qIYwMNtEelktzXP_pD5", "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK", "EQAU35_2hAbymisgUrhGa4bIJUtEJjVNVS7zBrqfKaENd67N", - "EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ": + "EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ", + "EQDCR0XQ0qNQJNjITRpo59mFsP0pjx81ImtXx92mJBnIc7m4", + "EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC": return true default: return false From 2bd964de15b429d735d380a252678ff1f4ace533 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 11:09:31 +0300 Subject: [PATCH 048/120] [repo] AddMessages: batch insert --- internal/core/repository/msg/msg.go | 45 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 6bfe62bb..92af6bd1 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -174,32 +174,33 @@ func (r *Repository) AddMessages(ctx context.Context, tx bun.Tx, messages []*cor if len(messages) == 0 { return nil } - for _, msg := range messages { // TODO: on conflict does not work with array (bun bug) - // some external messages can be repeated with the same hash - - // if some message has been already inserted, - // we update destination transaction and parsed data - - _, err := tx.NewInsert().Model(msg). - On("CONFLICT (hash) DO UPDATE"). - Set("dst_tx_lt = ?dst_tx_lt"). - Set("dst_workchain = ?dst_workchain"). - Set("dst_shard = ?dst_shard"). - Set("dst_block_seq_no = ?dst_block_seq_no"). - Set("src_contract = ?src_contract"). - Set("dst_contract = ?dst_contract"). - Set("operation_name = ?operation_name"). - Set("data_json = ?data_json"). - Set("error = ?error"). - Exec(ctx) - if err != nil { - return err - } + + // some external messages can be repeated with the same hash + + // if some message has been already inserted, + // we update destination transaction and parsed data + + _, err := tx.NewInsert().Model(&messages). + On("CONFLICT (hash) DO UPDATE"). + Set("dst_tx_lt = EXCLUDED.dst_tx_lt"). + Set("dst_workchain = EXCLUDED.dst_workchain"). + Set("dst_shard = EXCLUDED.dst_shard"). + Set("dst_block_seq_no = EXCLUDED.dst_block_seq_no"). + Set("src_contract = EXCLUDED.src_contract"). + Set("dst_contract = EXCLUDED.dst_contract"). + Set("operation_name = EXCLUDED.operation_name"). + Set("data_json = EXCLUDED.data_json"). + Set("error = EXCLUDED.error"). + Exec(ctx) + if err != nil { + return err } - _, err := r.ch.NewInsert().Model(&messages).Exec(ctx) + + _, err = r.ch.NewInsert().Model(&messages).Exec(ctx) if err != nil { return err } + return nil } From a722096ec70bfe1e865e1f13765eef9b838d837b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 13:24:29 +0300 Subject: [PATCH 049/120] [indexer] insert blocks in batches --- internal/app/indexer/save.go | 61 +++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 786fc786..0c6c1234 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -195,12 +195,25 @@ func (s *Service) uniqMessages(ctx context.Context, transactions []*core.Transac var lastLog = time.Now() -func (s *Service) saveBlock(ctx context.Context, master *core.Block) { - newBlocks := append([]*core.Block{master}, master.Shards...) +func (s *Service) saveBlocks(ctx context.Context, masterBlocks []*core.Block) { + var ( + newBlocks []*core.Block + newTransactions []*core.Transaction + lastSeqNo uint32 + ) + + for _, master := range masterBlocks { + if master.SeqNo > lastSeqNo { + lastSeqNo = master.SeqNo + } + + newBlocks = append(newBlocks, master) + newBlocks = append(newBlocks, master.Shards...) - var newTransactions []*core.Transaction - for i := range newBlocks { - newTransactions = append(newTransactions, newBlocks[i].Transactions...) + newTransactions = append(newTransactions, master.Transactions...) + for i := range master.Shards { + newTransactions = append(newTransactions, master.Shards[i].Transactions...) + } } if err := s.insertData(ctx, s.uniqAccounts(newTransactions), s.uniqMessages(ctx, newTransactions), newTransactions, newBlocks); err != nil { @@ -212,7 +225,10 @@ func (s *Service) saveBlock(ctx context.Context, master *core.Block) { lvl = log.Info() lastLog = time.Now() } - lvl.Uint32("last_inserted_seq", master.SeqNo).Msg("inserted new block") + lvl. + Int("master_blocks_len", len(masterBlocks)). + Uint32("last_inserted_seq", lastSeqNo). + Msg("inserted new block") } func (s *Service) saveBlocksLoop(results <-chan *core.Block) { @@ -220,20 +236,27 @@ func (s *Service) saveBlocksLoop(results <-chan *core.Block) { defer t.Stop() for s.running() { - var b *core.Block - - select { - case b = <-results: - case <-t.C: - continue + var blocks []*core.Block + + _loop: + for { + select { + case b := <-results: + log.Debug(). + Uint32("master_seq_no", b.SeqNo). + Int("master_tx", len(b.Transactions)). + Int("shards", len(b.Shards)). + Msg("new master") + + blocks = append(blocks, b) + + case <-t.C: + break _loop + } } - log.Debug(). - Uint32("master_seq_no", b.SeqNo). - Int("master_tx", len(b.Transactions)). - Int("shards", len(b.Shards)). - Msg("new master") - - s.saveBlock(context.Background(), b) + if len(blocks) != 0 { + s.saveBlocks(context.Background(), blocks) + } } } From 25261da2486799bc36ec5b3d109bfa3643b22935 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 13:33:03 +0300 Subject: [PATCH 050/120] [indexer] saveBlocks: add context with timeout --- internal/app/indexer/save.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 0c6c1234..81c0f2aa 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -216,6 +216,9 @@ func (s *Service) saveBlocks(ctx context.Context, masterBlocks []*core.Block) { } } + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + if err := s.insertData(ctx, s.uniqAccounts(newTransactions), s.uniqMessages(ctx, newTransactions), newTransactions, newBlocks); err != nil { panic(err) } From 4b0cc2d3cb522e7640d8cab7cb890504e48a3051 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 13:42:46 +0300 Subject: [PATCH 051/120] [repo] ConnectDB: add write timeout for pg --- internal/core/repository/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/db.go b/internal/core/repository/db.go index cd102645..3957cf27 100644 --- a/internal/core/repository/db.go +++ b/internal/core/repository/db.go @@ -39,7 +39,7 @@ func ConnectDB(ctx context.Context, dsnCH, dsnPG string, opts ...ch.Option) (*DB return nil, errors.Wrap(err, "cannot ping ch") } - sqlDB := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))) + sqlDB := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG), pgdriver.WithWriteTimeout(time.Minute))) pgDB := bun.NewDB(sqlDB, pgdialect.New()) for i := 0; i < 8; i++ { // wait for pg start From 888b39ecc1ad55cf22edab10539f21cff8896339 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 16:16:42 +0300 Subject: [PATCH 052/120] [indexer] insertData: context with timeout before inserts --- internal/app/indexer/save.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 81c0f2aa..cf6d2dc4 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -46,6 +46,9 @@ func (s *Service) insertData( } } + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + if err := func() error { defer core.Timer(time.Now(), "AddAccountStates(%d)", len(acc)) return s.accountRepo.AddAccountStates(ctx, dbTx, acc) @@ -216,9 +219,6 @@ func (s *Service) saveBlocks(ctx context.Context, masterBlocks []*core.Block) { } } - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - if err := s.insertData(ctx, s.uniqAccounts(newTransactions), s.uniqMessages(ctx, newTransactions), newTransactions, newBlocks); err != nil { panic(err) } From bebf1bc1e6d97224e2c781ae6526c5104c3a8651 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 16:23:13 +0300 Subject: [PATCH 053/120] [indexer] uniqMessages: add timer --- internal/app/indexer/save.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index cf6d2dc4..d9b39f18 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -168,6 +168,8 @@ func (s *Service) getMessageSource(ctx context.Context, msg *core.Message) (skip } func (s *Service) uniqMessages(ctx context.Context, transactions []*core.Transaction) []*core.Message { + defer core.Timer(time.Now(), "uniqMessages(%d)", len(transactions)) + var ret []*core.Message uniqMsg := make(map[string]*core.Message) From 4c59abca1a3781e175a9c87360a0a8c8236e586c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 29 Aug 2024 16:51:26 +0300 Subject: [PATCH 054/120] [indexer] refactor uniqMessages to optimize message source check --- internal/app/indexer/save.go | 76 ++++++++++++++++++----------- internal/core/msg.go | 1 - internal/core/repository/msg/msg.go | 24 +-------- 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index d9b39f18..8f8a9f1c 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -134,37 +134,57 @@ func (s *Service) addMessage(msg *core.Message, uniqMsg map[string]*core.Message } } -func (s *Service) getMessageSource(ctx context.Context, msg *core.Message) (skip bool) { - source, err := s.msgRepo.GetMessage(context.Background(), msg.Hash) - if err == nil { - msg.SrcTxLT, msg.SrcShard, msg.SrcBlockSeqNo, msg.SrcState = - source.SrcTxLT, source.SrcShard, source.SrcBlockSeqNo, source.SrcState - return false - } - if err != nil && !errors.Is(err, core.ErrNotFound) { - panic(errors.Wrapf(err, "get message with hash %x", msg.Hash)) +func (s *Service) getMessagesSource(ctx context.Context, messages []*core.Message) (valid []*core.Message) { + var checkSourceHashes [][]byte + for _, msg := range messages { + checkSourceHashes = append(checkSourceHashes, msg.Hash) } - // some masterchain messages does not have source - if msg.SrcAddress.Workchain() == -1 && msg.DstAddress.Workchain() == -1 { - return false + sources, err := s.msgRepo.GetMessages(context.Background(), checkSourceHashes) + if err != nil { + panic(errors.Wrap(err, "get messages")) } - blocks, err := s.blockRepo.CountMasterBlocks(ctx) - if err != nil { - panic(errors.Wrap(err, "count masterchain blocks")) + messageSourceMap := make(map[string]*core.Message) + for _, msg := range sources { + messageSourceMap[string(msg.Hash)] = msg } - if blocks < 1000 { - log.Debug(). - Hex("dst_tx_hash", msg.DstTxHash). - Int32("dst_workchain", msg.DstWorkchain).Int64("dst_shard", msg.DstShard).Uint32("dst_block_seq_no", msg.DstBlockSeqNo). - Str("src_address", msg.SrcAddress.String()).Str("dst_address", msg.DstAddress.String()). - Msg("cannot find source message") - return true + + totalBlocks := -1 + for _, msg := range messages { + if source, ok := messageSourceMap[string(msg.Hash)]; ok { + msg.SrcTxLT, msg.SrcShard, msg.SrcBlockSeqNo, msg.SrcState = + source.SrcTxLT, source.SrcShard, source.SrcBlockSeqNo, source.SrcState + valid = append(valid, msg) + continue + } + + // some masterchain messages does not have source + if msg.SrcAddress.Workchain() == -1 && msg.DstAddress.Workchain() == -1 { + valid = append(valid, msg) + continue + } + + if totalBlocks == -1 { + totalBlocks, err = s.blockRepo.CountMasterBlocks(ctx) + if err != nil { + panic(errors.Wrap(err, "count masterchain blocks")) + } + } + if totalBlocks < 1000 { + log.Debug(). + Hex("dst_tx_hash", msg.DstTxHash). + Int32("dst_workchain", msg.DstWorkchain).Int64("dst_shard", msg.DstShard).Uint32("dst_block_seq_no", msg.DstBlockSeqNo). + Str("src_address", msg.SrcAddress.String()).Str("dst_address", msg.DstAddress.String()). + Msg("cannot find source message") + continue + } + + panic(fmt.Errorf("unknown source of message with dst tx hash %x on block (%d, %d, %d) from %s to %s", + msg.DstTxHash, msg.DstWorkchain, msg.DstShard, msg.DstBlockSeqNo, msg.SrcAddress.String(), msg.DstAddress.String())) } - panic(fmt.Errorf("unknown source of message with dst tx hash %x on block (%d, %d, %d) from %s to %s", - msg.DstTxHash, msg.DstWorkchain, msg.DstShard, msg.DstBlockSeqNo, msg.SrcAddress.String(), msg.DstAddress.String())) + return valid } func (s *Service) uniqMessages(ctx context.Context, transactions []*core.Transaction) []*core.Message { @@ -185,17 +205,17 @@ func (s *Service) uniqMessages(ctx context.Context, transactions []*core.Transac } } + var checkSourceMessages []*core.Message for _, msg := range uniqMsg { if msg.Type == core.Internal && (msg.SrcTxLT == 0 && msg.DstTxLT != 0) { - if s.getMessageSource(ctx, msg) { - continue - } + checkSourceMessages = append(checkSourceMessages, msg) + continue } ret = append(ret, msg) } - return ret + return append(ret, s.getMessagesSource(ctx, checkSourceMessages)...) } var lastLog = time.Now() diff --git a/internal/core/msg.go b/internal/core/msg.go index c63511dd..965827b2 100644 --- a/internal/core/msg.go +++ b/internal/core/msg.go @@ -81,7 +81,6 @@ type MessageRepository interface { AddMessages(ctx context.Context, tx bun.Tx, messages []*Message) error UpdateMessages(ctx context.Context, messages []*Message) error - GetMessage(ctx context.Context, hash []byte) (*Message, error) GetMessages(ctx context.Context, hash [][]byte) ([]*Message, error) // MatchMessagesByOperationDesc returns hashes of suitable messages for the given contract operation. diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 92af6bd1..2a8444b3 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -2,7 +2,6 @@ package msg import ( "context" - "database/sql" "fmt" "strings" @@ -243,33 +242,14 @@ func (r *Repository) UpdateMessages(ctx context.Context, messages []*core.Messag return nil } -func (r *Repository) GetMessage(ctx context.Context, hash []byte) (*core.Message, error) { - var ret core.Message - - err := r.pg.NewSelect().Model(&ret). - Relation("SrcState"). - Relation("DstState"). - Where("hash = ?", hash). - Scan(ctx) - if errors.Is(err, sql.ErrNoRows) { - return nil, core.ErrNotFound - } - if err != nil { - return nil, err - } - - return &ret, nil -} - func (r *Repository) GetMessages(ctx context.Context, hashes [][]byte) ([]*core.Message, error) { var ret []*core.Message err := r.pg.NewSelect().Model(&ret). + Relation("SrcState"). + Relation("DstState"). Where("hash IN (?)", bun.In(hashes)). Scan(ctx) - if errors.Is(err, sql.ErrNoRows) { - return nil, core.ErrNotFound - } if err != nil { return nil, err } From e89173975fc4a9c4996c4cec1edfde7c8b9cfca8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 30 Sep 2024 21:31:56 +0800 Subject: [PATCH 055/120] [indexer] publishProcessedBlocks: fix slice copying --- internal/app/indexer/fetch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index 6e8ca21f..fdf51e31 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -149,7 +149,7 @@ func publishProcessedBlocks(fromBlock uint32, processed []*core.Block, results c fromBlock++ - copy(processed[:it], processed[it+1:]) + copy(processed[it:], processed[it+1:]) processed = processed[:len(processed)-1] found = true From 7d3186b7f11dd30ba61a70d61394bbcc5fca7d81 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 4 Oct 2024 19:32:28 +0800 Subject: [PATCH 056/120] [query] GetStatistics: fix method to only one thread, add cache --- internal/app/query/query.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index efa63bb7..38b1ca54 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -2,6 +2,7 @@ package query import ( "context" + "sync" "github.com/pkg/errors" "github.com/xssnick/tonutils-go/ton" @@ -32,6 +33,10 @@ type Service struct { txRepo repository.Transaction msgRepo repository.Message accountRepo repository.Account + + statsLastBlock uint32 + statsCached *aggregate.Statistics + statsLock sync.RWMutex } func NewService(_ context.Context, cfg *app.QueryConfig) (*Service, error) { @@ -53,7 +58,27 @@ func (s *Service) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFi } func (s *Service) GetStatistics(ctx context.Context) (*aggregate.Statistics, error) { - return aggregate.GetStatistics(ctx, s.DB.CH, s.DB.PG) + s.statsLock.RLock() + defer s.statsLock.RUnlock() + + m, err := s.blockRepo.GetLastMasterBlock(ctx) + if err != nil { + return nil, err + } + + if s.statsCached != nil && m.SeqNo == s.statsLastBlock { + return s.statsCached, nil + } + + stats, err := aggregate.GetStatistics(ctx, s.DB.CH, s.DB.PG) + if err != nil { + return nil, err + } + + s.statsCached = stats + s.statsLastBlock = uint32(stats.LastBlock) + + return stats, nil } func (s *Service) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) { From 677921d4ca7a1e6b553089c3aea446399cdb411a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 26 Oct 2024 18:16:43 +0800 Subject: [PATCH 057/120] [core] SkipAddress: add gemz, wonton --- internal/core/account.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index 74234787..4f06c8ac 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -184,6 +184,10 @@ func SkipAddress(a addr.Address) bool { "EQDCR0XQ0qNQJNjITRpo59mFsP0pjx81ImtXx92mJBnIc7m4", "EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC": return true + case "EQC_0ScHnb7bVoyInXLkZ2G4XRHg97S9XrPKCUDaO1ZRyFhZ": // Gemz Checkin + return true + case "EQD_QUnVTBzwG-8GCkqnQ4xiWxU0oPZn9Pon_rq0MZVdIBuf": // Wonton (?) + return true default: return false } From 35811ab4b6868c4ed88ae94f7c296aebfb3c12a6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Dec 2024 17:42:14 +0700 Subject: [PATCH 058/120] Dockerfile: apt-get install autoconf libtool --- Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 659db95c..9bb26a4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,11 @@ ENV TZ=Etc/UTC RUN apt-get update && \ apt-get install -yqq \ - tzdata build-essential cmake clang openssl \ - libssl-dev zlib1g-dev gperf wget git curl \ - libreadline-dev ccache libmicrohttpd-dev ninja-build pkg-config \ - libsecp256k1-dev libsodium-dev liblz4-dev + tzdata build-essential cmake clang openssl \ + autoconf libtool \ + libssl-dev zlib1g-dev gperf wget git curl \ + libreadline-dev ccache libmicrohttpd-dev ninja-build pkg-config \ + libsecp256k1-dev libsodium-dev liblz4-dev ADD --keep-git-dir=true https://github.com/ton-blockchain/ton.git /ton RUN cd /ton && git submodule update --init --recursive From b260164ab5212a6b99d9df3334fb5189e8a16fce Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 13 Jan 2025 13:28:14 +0100 Subject: [PATCH 059/120] [core] SkipAddress: add more addresses --- internal/core/account.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index 4f06c8ac..2e05b573 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -176,17 +176,26 @@ func SkipAddress(a addr.Address) bool { return true case "EQDF6fj6ydJJX_ArwxINjP-0H8zx982W4XgbkKzGvceUWvXl": // ETH Token Bridge Collector return true + case "EQC_0ScHnb7bVoyInXLkZ2G4XRHg97S9XrPKCUDaO1ZRyFhZ": // Gemz Checkin + return true + case "EQD_QUnVTBzwG-8GCkqnQ4xiWxU0oPZn9Pon_rq0MZVdIBuf", + "EQB2MfIcTbwtshE8VOv0YA6ZWpb9bbj79D_SUXHZYv04X47c": // Wonton (?) + return true + case "EQAqk4SStGaodBsjW0zc8H4psrsx258cCdqw4Nm3ScnMYpLf": // some service (?) + return true + case "EQDlHrYvmV9R91wNbqvpzo-_pXu4Q6vQZo0-t2CplC6Zgh4y": // RBT trader + return true + case "EQD5iFPj0zk1mA-GatG_3QtBNWVzuRKatszH1MUYAw6aVeK2": // some service claims (?) + return true case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7", "EQAlMRLTYOoG6kM0d3dLHqgK30ol3qIYwMNtEelktzXP_pD5", "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK", "EQAU35_2hAbymisgUrhGa4bIJUtEJjVNVS7zBrqfKaENd67N", "EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ", "EQDCR0XQ0qNQJNjITRpo59mFsP0pjx81ImtXx92mJBnIc7m4", - "EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC": - return true - case "EQC_0ScHnb7bVoyInXLkZ2G4XRHg97S9XrPKCUDaO1ZRyFhZ": // Gemz Checkin - return true - case "EQD_QUnVTBzwG-8GCkqnQ4xiWxU0oPZn9Pon_rq0MZVdIBuf": // Wonton (?) + "EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC", + "EQD-r3joXyZ2kWRxraqze6ypKoVtSx1qlKlJsNEjyLM7ujs7", + "EQDTCD85dI5Cu8O1eDecuARaagwaOPMacnXwqn8KB0-1DN8P": // unknown return true default: return false From 49496332554141d63fe955f9acf538954dd1bc09 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 1 Mar 2025 21:56:14 +0000 Subject: [PATCH 060/120] [query] calculate statistics in background --- internal/app/query/query.go | 65 +++++++++++++++++++++++++------------ internal/app/query/stats.go | 63 +++++++++++++++++++++++++++++++++++ internal/core/errors.go | 1 + 3 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 internal/app/query/stats.go diff --git a/internal/app/query/query.go b/internal/app/query/query.go index 38b1ca54..2cb1fd9a 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -3,6 +3,7 @@ package query import ( "context" "sync" + "time" "github.com/pkg/errors" "github.com/xssnick/tonutils-go/ton" @@ -34,9 +35,13 @@ type Service struct { msgRepo repository.Message accountRepo repository.Account - statsLastBlock uint32 - statsCached *aggregate.Statistics - statsLock sync.RWMutex + statsCached *aggregate.Statistics + statsUpdateTs time.Time + statsFailTs time.Time + + run bool + mx sync.RWMutex + wg sync.WaitGroup } func NewService(_ context.Context, cfg *app.QueryConfig) (*Service, error) { @@ -53,32 +58,50 @@ func NewService(_ context.Context, cfg *app.QueryConfig) (*Service, error) { return s, nil } -func (s *Service) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { - return s.contractRepo.GetDefinitions(ctx) +func (s *Service) running() bool { + s.mx.RLock() + defer s.mx.RUnlock() + + return s.run } -func (s *Service) GetStatistics(ctx context.Context) (*aggregate.Statistics, error) { - s.statsLock.RLock() - defer s.statsLock.RUnlock() +func (s *Service) Start() error { + s.mx.Lock() + defer s.mx.Unlock() - m, err := s.blockRepo.GetLastMasterBlock(ctx) - if err != nil { - return nil, err + if s.run { + return core.ErrAlreadyExists } - if s.statsCached != nil && m.SeqNo == s.statsLastBlock { - return s.statsCached, nil - } + s.run = true - stats, err := aggregate.GetStatistics(ctx, s.DB.CH, s.DB.PG) - if err != nil { - return nil, err - } + s.wg.Add(1) + go s.updateStatsLoop() + + return nil +} + +func (s *Service) Stop() { + s.mx.Lock() + s.run = false + s.mx.Unlock() - s.statsCached = stats - s.statsLastBlock = uint32(stats.LastBlock) + s.wg.Wait() +} + +func (s *Service) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { + return s.contractRepo.GetDefinitions(ctx) +} + +func (s *Service) GetStatistics(_ context.Context) (*aggregate.Statistics, error) { + s.mx.RLock() + defer s.mx.RUnlock() + + if s.statsCached == nil || time.Since(s.statsUpdateTs) > statsLifespan { + return nil, core.ErrNotAvailable + } - return stats, nil + return s.statsCached, nil } func (s *Service) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) { diff --git a/internal/app/query/stats.go b/internal/app/query/stats.go new file mode 100644 index 00000000..d17deb2d --- /dev/null +++ b/internal/app/query/stats.go @@ -0,0 +1,63 @@ +package query + +import ( + "context" + "time" + + "github.com/rs/zerolog/log" + + "github.com/tonindexer/anton/internal/core/aggregate" +) + +const ( + statsRetryDelay = 15 * time.Second + statsUpdateDelay = 5 * time.Minute + statsLifespan = 10 * time.Minute + statsCalculationTimeout = 2 * time.Minute +) + +func (s *Service) updateStatsLoop() { + defer s.wg.Done() + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if !s.running() { + return + } + + s.mx.RLock() + lastUpdate := s.statsUpdateTs + lastTry := s.statsFailTs + s.mx.RUnlock() + + if time.Since(lastUpdate) > statsUpdateDelay && time.Since(lastTry) > statsRetryDelay { + s.updateStats() + } + } + } +} + +func (s *Service) updateStats() { + ctx, cancel := context.WithTimeout(context.Background(), statsCalculationTimeout) + defer cancel() + + stats, err := aggregate.GetStatistics(ctx, s.DB.CH, s.DB.PG) + + s.mx.Lock() + defer s.mx.Unlock() + + if err != nil { + log.Error().Err(err).Msg("cannot update stats") + s.statsFailTs = time.Now() + return + } + + s.statsCached = stats + s.statsUpdateTs = time.Now() + + return +} diff --git a/internal/core/errors.go b/internal/core/errors.go index 32cfefe6..82239ef1 100644 --- a/internal/core/errors.go +++ b/internal/core/errors.go @@ -6,5 +6,6 @@ var ( ErrNotFound = errors.New("not found") ErrInvalidArg = errors.New("invalid arguments") ErrNotImplemented = errors.New("not implemented") + ErrNotAvailable = errors.New("not available") ErrAlreadyExists = errors.New("already exists") ) From 90259b34f6fd605b5d5ea0790bd52a0728cc24c3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 1 Mar 2025 22:05:41 +0000 Subject: [PATCH 061/120] [cmd] query: start update stats loop --- cmd/web/web.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/web/web.go b/cmd/web/web.go index 9ae240e4..ae927cd8 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -64,6 +64,9 @@ var Command = &cli.Command{ if err != nil { return err } + if err := qs.Start(); err != nil { + return err + } srv := http.NewServer( env.GetString("LISTEN", "0.0.0.0:80"), @@ -74,6 +77,7 @@ var Command = &cli.Command{ signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) go func() { <-c + qs.Stop() conn.Close() os.Exit(0) }() From ca5a6f7f4179f9cda33687d3e84dc2a73444937d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 20 Jun 2025 19:33:23 +0300 Subject: [PATCH 062/120] [repo] message: add cache for messages count --- cmd/db/db.go | 8 +- internal/app/fetcher/account.go | 4 +- internal/app/rescan/rescan.go | 7 +- internal/core/filter/account.go | 13 ++- internal/core/filter/block.go | 6 +- internal/core/filter/cache.go | 75 ++++++++++++++++ internal/core/filter/msg.go | 10 ++- internal/core/filter/tx.go | 6 +- .../core/repository/account/filter_test.go | 70 +++++++++------ internal/core/repository/block/filter_test.go | 14 ++- internal/core/repository/msg/filter.go | 88 ++++++++++++++++--- internal/core/repository/msg/filter_test.go | 20 ++++- internal/core/repository/msg/msg.go | 13 ++- internal/core/repository/repository_test.go | 22 +++-- internal/core/repository/tx/filter_test.go | 35 +++++--- 15 files changed, 311 insertions(+), 80 deletions(-) create mode 100644 internal/core/filter/cache.go diff --git a/cmd/db/db.go b/cmd/db/db.go index 8c8bb51a..e68995bc 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -502,9 +502,11 @@ var Command = &cli.Command{ for i := range blockIds { res, err := blockRepo.FilterBlocks(c.Context, &filter.BlocksReq{ - Workchain: &m.Workchain, - Shard: &m.Shard, - SeqNo: &blockIds[i], + BlocksFilter: filter.BlocksFilter{ + Workchain: &m.Workchain, + Shard: &m.Shard, + SeqNo: &blockIds[i], + }, WithShards: true, WithAccountStates: true, WithTransactions: true, diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 6b6ccfa7..d6957144 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -24,8 +24,10 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l lastLT++ accountReq := filter.AccountsReq{ + AccountsFilter: filter.AccountsFilter{ + Addresses: []*addr.Address{&a}, + }, WithCodeData: true, - Addresses: []*addr.Address{&a}, Order: "DESC", AfterTxLT: &lastLT, Limit: 1, diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 466b3409..fb320da5 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -188,7 +188,12 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro } func (s *Service) rescanAccounts(ctx context.Context, task *core.RescanTask, ids []*core.AccountStateID) error { - accRet, err := s.AccountRepo.FilterAccounts(ctx, &filter.AccountsReq{WithCodeData: true, StateIDs: ids}) + accRet, err := s.AccountRepo.FilterAccounts(ctx, &filter.AccountsReq{ + AccountsFilter: filter.AccountsFilter{ + StateIDs: ids, + }, + WithCodeData: true, + }) if err != nil { return errors.Wrapf(err, "filter accounts") } diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index cc531374..d2ffae28 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -20,11 +20,10 @@ type LabelsRes struct { Rows []*core.AddressLabel `json:"results"` } -type AccountsReq struct { - WithCodeData bool +type AccountsFilter struct { + LatestState bool `form:"latest"` - Addresses []*addr.Address // `form:"addresses"` - LatestState bool `form:"latest"` + Addresses []*addr.Address // `form:"addresses"` StateIDs []*core.AccountStateID @@ -38,6 +37,12 @@ type AccountsReq struct { ContractTypes []abi.ContractName `form:"interface"` OwnerAddress *addr.Address // `form:"owner_address"` MinterAddress *addr.Address // `form:"minter_address"` +} + +type AccountsReq struct { + AccountsFilter + + WithCodeData bool ExcludeColumn []string // TODO: support relations diff --git a/internal/core/filter/block.go b/internal/core/filter/block.go index 51e39244..99de6b0a 100644 --- a/internal/core/filter/block.go +++ b/internal/core/filter/block.go @@ -6,11 +6,15 @@ import ( "github.com/tonindexer/anton/internal/core" ) -type BlocksReq struct { +type BlocksFilter struct { Workchain *int32 `form:"workchain"` Shard *int64 `form:"shard"` SeqNo *uint32 `form:"seq_no"` FileHash []byte `form:"file_hash"` +} + +type BlocksReq struct { + BlocksFilter WithShards bool // TODO: array of relations as strings WithAccountStates bool diff --git a/internal/core/filter/cache.go b/internal/core/filter/cache.go new file mode 100644 index 00000000..1f51381d --- /dev/null +++ b/internal/core/filter/cache.go @@ -0,0 +1,75 @@ +package filter + +import ( + "encoding/json" + "sync" + "time" + + "github.com/tonindexer/anton/internal/core" +) + +type CacheEntry struct { + Count int + MaxSeqNo uint64 + UpdatedAt time.Time +} + +type Cache struct { + msgCountCache map[string]CacheEntry + msgCountCacheMx sync.Mutex + msgCountCacheTTL time.Duration +} + +func NewCache(ttl time.Duration) *Cache { + return &Cache{ + msgCountCache: make(map[string]CacheEntry), + msgCountCacheTTL: ttl, + } +} + +func getCacheKey(req any) (string, error) { + bytes, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func (c *Cache) Set(filterReq any, count int, maxSeqNo uint64) error { + k, err := getCacheKey(filterReq) + if err != nil { + return err + } + + c.msgCountCacheMx.Lock() + defer c.msgCountCacheMx.Unlock() + + c.msgCountCache[k] = CacheEntry{ + Count: count, + MaxSeqNo: maxSeqNo, + UpdatedAt: time.Now(), + } + + return nil +} + +func (c *Cache) Get(filterReq any) (count int, maxSeqNo uint64, err error) { + k, err := getCacheKey(filterReq) + if err != nil { + return 0, 0, err + } + + c.msgCountCacheMx.Lock() + defer c.msgCountCacheMx.Unlock() + + entry, ok := c.msgCountCache[k] + if !ok { + return 0, 0, core.ErrNotFound + } + if time.Since(entry.UpdatedAt) > c.msgCountCacheTTL { + delete(c.msgCountCache, k) + return 0, 0, core.ErrNotFound + } + + return entry.Count, entry.MaxSeqNo, nil +} diff --git a/internal/core/filter/msg.go b/internal/core/filter/msg.go index 1b722841..567d4f23 100644 --- a/internal/core/filter/msg.go +++ b/internal/core/filter/msg.go @@ -9,9 +9,7 @@ import ( "github.com/tonindexer/anton/internal/core" ) -type MessagesReq struct { - DBTx *bun.Tx - +type MessagesFilter struct { Hash []byte // `form:"hash"` SrcAddresses []*addr.Address // `form:"src_address"` DstAddresses []*addr.Address // `form:"dst_address"` @@ -23,6 +21,12 @@ type MessagesReq struct { SrcContracts []string `form:"src_contract"` DstContracts []string `form:"dst_contract"` OperationNames []string `form:"operation_name"` +} + +type MessagesReq struct { + DBTx *bun.Tx + + MessagesFilter Order string `form:"order"` // ASC, DESC diff --git a/internal/core/filter/tx.go b/internal/core/filter/tx.go index d40055d7..ee629983 100644 --- a/internal/core/filter/tx.go +++ b/internal/core/filter/tx.go @@ -7,7 +7,7 @@ import ( "github.com/tonindexer/anton/internal/core" ) -type TransactionsReq struct { +type TransactionsFilter struct { Hash []byte // `form:"hash"` InMsgHash []byte // `form:"in_msg_hash"` @@ -16,6 +16,10 @@ type TransactionsReq struct { Workchain *int32 `form:"workchain"` BlockID *core.BlockID +} + +type TransactionsReq struct { + TransactionsFilter WithAccountState bool WithMessages bool diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index f658abca..a93b4e87 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -192,8 +192,10 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter states by address", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, - Addresses: []*addr.Address{address}, - Order: "ASC", Limit: len(addressStates), Count: true, + AccountsFilter: filter.AccountsFilter{ + Addresses: []*addr.Address{address}, + }, + Order: "ASC", Limit: len(addressStates), Count: true, }) require.Nil(t, err) require.Equal(t, 15, results.Total) @@ -205,9 +207,11 @@ func TestRepository_FilterAccounts(t *testing.T) { latest.Code = nil results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ - WithCodeData: true, - Addresses: []*addr.Address{&latest.Address}, - LatestState: true, + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + Addresses: []*addr.Address{&latest.Address}, + LatestState: true, + }, ExcludeColumn: []string{"code"}, Count: true, }) require.Nil(t, err) @@ -220,9 +224,11 @@ func TestRepository_FilterAccounts(t *testing.T) { latest.Code = nil results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ - WithCodeData: true, - Addresses: []*addr.Address{&latest.Address}, - LatestState: true, + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + Addresses: []*addr.Address{&latest.Address}, + LatestState: true, + }, ExcludeColumn: []string{"code"}, Count: true, }) require.Nil(t, err) @@ -232,10 +238,12 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter latest state with data by contract types", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ - WithCodeData: true, - ContractTypes: []abi.ContractName{"special", "some_nonsense"}, - LatestState: true, - Order: "DESC", Limit: 1, Count: true, + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + ContractTypes: []abi.ContractName{"special", "some_nonsense"}, + LatestState: true, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 15, results.Total) @@ -244,9 +252,11 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter states by minter", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ - WithCodeData: true, - MinterAddress: latestState.MinterAddress, - Order: "DESC", Limit: 1, Count: true, + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + MinterAddress: latestState.MinterAddress, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 5, results.Total) @@ -256,8 +266,10 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter states by owner", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, - OwnerAddress: latestState.OwnerAddress, - Order: "DESC", Limit: 1, Count: true, + AccountsFilter: filter.AccountsFilter{ + OwnerAddress: latestState.OwnerAddress, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -267,9 +279,11 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter latest states by owner", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, - LatestState: true, - OwnerAddress: latestState.OwnerAddress, - Order: "DESC", Limit: 1, Count: true, + AccountsFilter: filter.AccountsFilter{ + LatestState: true, + OwnerAddress: latestState.OwnerAddress, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) @@ -279,8 +293,10 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("filter by account state ids", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, - StateIDs: []*core.AccountStateID{{Address: latestState.Address, LastTxLT: latestState.LastTxLT}}, - Order: "DESC", Limit: 1, Count: true, + AccountsFilter: filter.AccountsFilter{ + StateIDs: []*core.AccountStateID{{Address: latestState.Address, LastTxLT: latestState.LastTxLT}}, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 0, results.Total) @@ -363,10 +379,12 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { start := time.Now() results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ - WithCodeData: true, - ContractTypes: []abi.ContractName{"special"}, - LatestState: true, - Order: "DESC", Limit: 1, Count: true, + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + ContractTypes: []abi.ContractName{"special"}, + LatestState: true, + }, + Order: "DESC", Limit: 1, Count: true, }) require.Nil(t, err) require.Equal(t, 1, results.Total) diff --git a/internal/core/repository/block/filter_test.go b/internal/core/repository/block/filter_test.go index 7159f7be..fd5b88d9 100644 --- a/internal/core/repository/block/filter_test.go +++ b/internal/core/repository/block/filter_test.go @@ -78,7 +78,9 @@ func TestRepository_FilterBlocks(t *testing.T) { t.Run("filter by workchain", func(t *testing.T) { res, err := repo.FilterBlocks(ctx, &filter.BlocksReq{ - Workchain: &shard.Workchain, + BlocksFilter: filter.BlocksFilter{ + Workchain: &shard.Workchain, + }, // Shard: &shard.Shard, // SeqNo: &shard.SeqNo, @@ -91,8 +93,10 @@ func TestRepository_FilterBlocks(t *testing.T) { t.Run("filter by seq no", func(t *testing.T) { res, err := repo.FilterBlocks(ctx, &filter.BlocksReq{ - Workchain: &shard.Workchain, - SeqNo: &shard.SeqNo, + BlocksFilter: filter.BlocksFilter{ + Workchain: &shard.Workchain, + SeqNo: &shard.SeqNo, + }, AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, Count: true, }) @@ -103,7 +107,9 @@ func TestRepository_FilterBlocks(t *testing.T) { t.Run("filter by file hash", func(t *testing.T) { res, err := repo.FilterBlocks(ctx, &filter.BlocksReq{ - FileHash: master.FileHash, + BlocksFilter: filter.BlocksFilter{ + FileHash: master.FileHash, + }, WithShards: true, diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 938ab1e0..b14ed4db 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/pkg/errors" "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" @@ -11,14 +12,7 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (ret []*core.Message, err error) { - q := r.pg.NewSelect() - if req.DBTx != nil { - q = req.DBTx.NewSelect() - } - - q = q.Model(&ret) - +func (r *Repository) getFilterMessageQuery(q *bun.SelectQuery, req *filter.MessagesFilter) *bun.SelectQuery { if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) } @@ -47,6 +41,20 @@ func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (re if len(req.OperationNames) > 0 { q = q.Where("operation_name IN (?)", bun.In(req.OperationNames)) } + + return q +} + +func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (ret []*core.Message, err error) { + q := r.pg.NewSelect() + if req.DBTx != nil { + q = req.DBTx.NewSelect() + } + + q = q.Model(&ret) + + q = r.getFilterMessageQuery(q, &req.MessagesFilter) + if req.AfterTxLT != nil { if req.Order == "ASC" { q = q.Where("created_lt > ?", req.AfterTxLT) @@ -68,9 +76,16 @@ func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (re return ret, err } -func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int, error) { +func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesReq) (count int, maxLt uint64, err error) { + var result struct { + Count int + MaxLT uint64 `ch:"max_lt"` + } + q := r.ch.NewSelect(). - Model((*core.Message)(nil)) + Model((*core.Message)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("max(created_lt) AS max_lt") if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) @@ -100,7 +115,58 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int q = q.Where("operation_name IN (?)", ch.In(req.OperationNames)) } - return q.Count(ctx) + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + return result.Count, result.MaxLT, nil +} + +func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.MessagesReq, startLt uint64) (partialCount int, maxLt uint64, err error) { + var result struct { + Count int + MaxLT uint64 `ch:"max_lt"` + } + + q := r.pg.NewSelect(). + Model((*core.Message)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("max(created_lt) AS max_lt"). + Where("created_lt > ?", startLt) + + q = r.getFilterMessageQuery(q, &req.MessagesFilter) + + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + return result.Count, result.MaxLT, nil +} + +func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int, error) { + count, maxLT, err := r.messagesFilterCache.Get(req.MessagesFilter) + if errors.Is(err, core.ErrNotFound) { + count, maxLT, err = r.countMsgFullScan(ctx, req) + if err != nil { + return 0, err + } + if err := r.messagesFilterCache.Set(req.MessagesFilter, count, maxLT); err != nil { + return 0, err + } + } + if err != nil && !errors.Is(err, core.ErrNotFound) { + return 0, err + } + + partialCount, maxLT, err := r.countMsgPartialScan(ctx, req, maxLT) + if err != nil { + return 0, err + } + if err := r.messagesFilterCache.Set(req.MessagesFilter, count+partialCount, maxLT); err != nil { + return 0, err + } + + return count + partialCount, nil } func (r *Repository) FilterMessages(ctx context.Context, req *filter.MessagesReq) (*filter.MessagesRes, error) { diff --git a/internal/core/repository/msg/filter_test.go b/internal/core/repository/msg/filter_test.go index 6fc9de76..0d8ae371 100644 --- a/internal/core/repository/msg/filter_test.go +++ b/internal/core/repository/msg/filter_test.go @@ -50,7 +50,10 @@ func TestRepository_FilterMessages(t *testing.T) { expected := *messages[0] res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - Hash: messages[0].Hash, Count: true, + MessagesFilter: filter.MessagesFilter{ + Hash: messages[0].Hash, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -62,7 +65,10 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by address", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - DstAddresses: []*addr.Address{&messages[0].DstAddress}, Count: true, + MessagesFilter: filter.MessagesFilter{ + DstAddresses: []*addr.Address{&messages[0].DstAddress}, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -71,7 +77,10 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by contract", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - DstContracts: []string{"special"}, Count: true, + MessagesFilter: filter.MessagesFilter{ + DstContracts: []string{"special"}, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -83,7 +92,10 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by operation name", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ - OperationNames: []string{"special_op"}, Count: true, + MessagesFilter: filter.MessagesFilter{ + OperationNames: []string{"special_op"}, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 2a8444b3..fd773032 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -13,18 +14,24 @@ import ( "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" "github.com/tonindexer/anton/internal/core/repository" ) var _ repository.Message = (*Repository)(nil) type Repository struct { - ch *ch.DB - pg *bun.DB + ch *ch.DB + pg *bun.DB + messagesFilterCache *filter.Cache } func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { - return &Repository{ch: ck, pg: pg} + return &Repository{ + ch: ck, + pg: pg, + messagesFilterCache: filter.NewCache(7 * 24 * time.Hour), + } } func createIndexes(ctx context.Context, pgDB *bun.DB) error { diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index 377771b6..cc4670d4 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -205,9 +205,11 @@ func TestRelations(t *testing.T) { t.Run("get account states with data", func(t *testing.T) { res, err := accountRepo.FilterAccounts(ctx, &filter.AccountsReq{ - Addresses: addresses, - LatestState: true, - Count: true, + AccountsFilter: filter.AccountsFilter{ + Addresses: addresses, + LatestState: true, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -216,8 +218,10 @@ func TestRelations(t *testing.T) { t.Run("get messages with payloads", func(t *testing.T) { res, err := msgRepo.FilterMessages(ctx, &filter.MessagesReq{ - DstAddresses: addresses, - Count: true, + MessagesFilter: filter.MessagesFilter{ + DstAddresses: addresses, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -231,7 +235,9 @@ func TestRelations(t *testing.T) { t.Run("get transactions with states and messages", func(t *testing.T) { res, err := txRepo.FilterTransactions(ctx, &filter.TransactionsReq{ - Addresses: addresses, + TransactionsFilter: filter.TransactionsFilter{ + Addresses: addresses, + }, WithAccountState: true, WithMessages: true, Count: true, @@ -248,7 +254,9 @@ func TestRelations(t *testing.T) { t.Run("get master block with shards and transactions", func(t *testing.T) { var workchain int32 = -1 res, err := blockRepo.FilterBlocks(ctx, &filter.BlocksReq{ - Workchain: &workchain, + BlocksFilter: filter.BlocksFilter{ + Workchain: &workchain, + }, WithShards: true, WithTransactions: true, WithTransactionAccountState: true, diff --git a/internal/core/repository/tx/filter_test.go b/internal/core/repository/tx/filter_test.go index f72ca7c0..038793b3 100644 --- a/internal/core/repository/tx/filter_test.go +++ b/internal/core/repository/tx/filter_test.go @@ -42,7 +42,10 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - Hash: transactions[0].Hash, Count: true, + TransactionsFilter: filter.TransactionsFilter{ + Hash: transactions[0].Hash, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -51,7 +54,10 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by incoming message hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - InMsgHash: transactions[0].InMsgHash, Count: true, + TransactionsFilter: filter.TransactionsFilter{ + InMsgHash: transactions[0].InMsgHash, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -60,7 +66,10 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by addresses", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - Addresses: []*addr.Address{&transactions[0].Address}, Count: true, + TransactionsFilter: filter.TransactionsFilter{ + Addresses: []*addr.Address{&transactions[0].Address}, + }, + Count: true, }) require.Nil(t, err) require.Equal(t, 1, res.Total) @@ -69,10 +78,12 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by block id", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - BlockID: &core.BlockID{ - Workchain: transactions[0].Workchain, - Shard: transactions[0].Shard, - SeqNo: transactions[0].BlockSeqNo, + TransactionsFilter: filter.TransactionsFilter{ + BlockID: &core.BlockID{ + Workchain: transactions[0].Workchain, + Shard: transactions[0].Shard, + SeqNo: transactions[0].BlockSeqNo, + }, }, Count: true, }) @@ -83,10 +94,12 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("filter by workchain", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ - Workchain: new(int32), - Order: "ASC", - Limit: len(transactions), - Count: true, + TransactionsFilter: filter.TransactionsFilter{ + Workchain: new(int32), + }, + Order: "ASC", + Limit: len(transactions), + Count: true, }) require.Nil(t, err) require.Equal(t, len(transactions), res.Total) From 93f24ddcea31f0a283d90a4fc5d246d4c504a867 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 20 Jun 2025 21:00:00 +0300 Subject: [PATCH 063/120] [repo] account: simplify states counting on filter --- internal/core/repository/account/filter.go | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index c6f9da33..d57a605e 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -171,7 +171,7 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts return ret, err } -func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsReq) (int, error) { +func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsReq) (count int, err error) { q := r.ch.NewSelect().Model((*core.AccountState)(nil)) if len(f.Addresses) > 0 { @@ -201,24 +201,26 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR q = q.Where("minter_address = ?", f.MinterAddress) } - if f.LatestState { - q = q.ColumnExpr("argMax(address, last_tx_lt)") - if f.OwnerAddress != nil { - q = q.ColumnExpr("argMax(owner_address, last_tx_lt) as owner_address") - } - q = q.Group("address") - } else { - q = q.Column("address") - if f.OwnerAddress != nil { - q = q.Column("owner_address") + if f.OwnerAddress != nil { + if f.LatestState { + q = r.ch.NewSelect().TableExpr("(?) as q", // because owner address can change + q.Column("address"). + ColumnExpr("argMax(owner_address, last_tx_lt) as owner_address"). + Group("address")). + Where("owner_address = ?", f.OwnerAddress) + } else { + q = q.Where("owner_address = ?", f.OwnerAddress) } } - qCount := r.ch.NewSelect().TableExpr("(?) as q", q) - if f.OwnerAddress != nil { // that's because owner address can change - qCount = qCount.Where("owner_address = ?", f.OwnerAddress) + if f.LatestState { + q = q.ColumnExpr("count(distinct address)") + } else { + q = q.ColumnExpr("count(*)") } - return qCount.Count(ctx) + + err = q.Scan(ctx, &count) + return count, err } func (r *Repository) getCodeData(ctx context.Context, rows []*core.AccountState, excludeCode, excludeData bool) error { //nolint:gocognit,gocyclo // TODO: make one function working for both code and data From d43812194b616addfbb748b7d99b76cb972eca56 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 20 Jun 2025 22:16:40 +0300 Subject: [PATCH 064/120] [core] write to the new table for latest parsed account states --- internal/core/account.go | 14 ++++ internal/core/repository/account/account.go | 78 +++++++++++++++++-- .../core/repository/account/account_test.go | 5 +- internal/core/repository/repository_test.go | 3 + ...3211_latest_parsed_account_states.down.sql | 0 ...183211_latest_parsed_account_states.up.sql | 28 +++++++ ...3211_latest_parsed_account_states.down.sql | 0 ...183211_latest_parsed_account_states.up.sql | 1 + 8 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 migrations/chmigrations/20250620183211_latest_parsed_account_states.down.sql create mode 100644 migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql create mode 100644 migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql create mode 100644 migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql diff --git a/internal/core/account.go b/internal/core/account.go index 2e05b573..fee33bd8 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -137,6 +137,20 @@ type LatestAccountState struct { AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` } +type LatestParsedAccountState struct { + bun.BaseModel `bun:"table:latest_parsed_account_states" json:"-"` + + Address addr.Address `bun:"type:bytea,pk,notnull" json:"address"` + LastTxLT uint64 `bun:"type:bigint,notnull" json:"last_tx_lt"` + + Types []abi.ContractName `bun:"type:text[],array" json:"types,omitempty"` + + OwnerAddress *addr.Address `bun:"type:bytea" json:"owner_address,omitempty"` // universal column for many contracts + MinterAddress *addr.Address `bun:"type:bytea" json:"minter_address,omitempty"` + + AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` +} + func SkipAddress(a addr.Address) bool { switch a.Base64() { case "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c": // burn address diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index e6911de3..3dee7617 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -104,6 +104,37 @@ func createIndexes(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "latest account state last_tx_lt pg create index") } + // latest parsed account state + + _, err = pgDB.NewCreateIndex(). + Model(&core.LatestParsedAccountState{}). + Using("HASH"). + Column("owner_address"). + Where("owner_address IS NOT NULL"). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "latest address state owner pg create index") + } + + _, err = pgDB.NewCreateIndex(). + Model(&core.LatestParsedAccountState{}). + Using("HASH"). + Column("minter_address"). + Where("minter_address IS NOT NULL"). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "latest address state minter pg create index") + } + + _, err = pgDB.NewCreateIndex(). + Model(&core.LatestParsedAccountState{}). + Using("GIN"). + Column("types"). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "account state contract types pg create index") + } + return nil } @@ -182,6 +213,15 @@ func CreateTables(ctx context.Context, chDB *ch.DB, pgDB *bun.DB) error { return errors.Wrap(err, "latest account state pg create table") } + _, err = pgDB.NewCreateTable(). + Model(&core.LatestParsedAccountState{}). + IfNotExists(). + WithForeignKeys(). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "latest token account state pg create table") + } + return createIndexes(ctx, pgDB) } @@ -247,26 +287,50 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ return errors.Wrapf(err, "cannot insert new account states") } - addrTxLT := make(map[addr.Address]uint64) - for _, a := range accounts { - if addrTxLT[a.Address] < a.LastTxLT { - addrTxLT[a.Address] = a.LastTxLT + latestStates := make(map[addr.Address]*core.AccountState) + for _, state := range accounts { + if latestStates[state.Address] == nil { + latestStates[state.Address] = state + } + if latestStates[state.Address].LastTxLT < state.LastTxLT { + latestStates[state.Address] = state } } - for a, lt := range addrTxLT { + for a, state := range latestStates { _, err := tx.NewInsert(). Model(&core.LatestAccountState{ Address: a, - LastTxLT: lt, + LastTxLT: state.LastTxLT, }). On("CONFLICT (address) DO UPDATE"). - Where("latest_account_state.last_tx_lt < ?", lt). + Where("latest_account_state.last_tx_lt < ?", state.LastTxLT). Set("last_tx_lt = EXCLUDED.last_tx_lt"). Exec(ctx) if err != nil { return errors.Wrapf(err, "cannot set latest state for %s", &a) } + + if state.OwnerAddress != nil || state.MinterAddress != nil || len(state.Types) > 0 { + _, err := tx.NewInsert(). + Model(&core.LatestParsedAccountState{ + Address: a, + LastTxLT: state.LastTxLT, + Types: state.Types, + OwnerAddress: state.OwnerAddress, + MinterAddress: state.MinterAddress, + }). + On("CONFLICT (address) DO UPDATE"). + Where("latest_parsed_account_state.last_tx_lt < ?", state.LastTxLT). + Set("last_tx_lt = EXCLUDED.last_tx_lt"). + Set("types = EXCLUDED.types"). + Set("owner_address = EXCLUDED.owner_address"). + Set("minter_address = EXCLUDED.minter_address"). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, "cannot set latest state for %s", &a) + } + } } _, err = r.ch.NewInsert().Model(&accounts).Exec(ctx) diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index 75564b3e..fd4fd1e7 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -57,7 +57,10 @@ func dropTables(t testing.TB) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - _, err := pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) + _, err := pg.NewDropTable().Model((*core.LatestParsedAccountState)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + + _, err = pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AccountStateCode)(nil)).IfExists().Exec(ctx) diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index cc4670d4..d23eb787 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -64,6 +64,9 @@ func dropTables(t testing.TB) { _, err = pg.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.LatestParsedAccountState)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) require.Nil(t, err) diff --git a/migrations/chmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/chmigrations/20250620183211_latest_parsed_account_states.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql new file mode 100644 index 00000000..9adcaa3f --- /dev/null +++ b/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -0,0 +1,28 @@ +--bun:split +CREATE TABLE latest_parsed_account_states ( + address bytea NOT NULL, + last_tx_lt bigint NOT NULL, + types text[], + owner_address bytea, + minter_address bytea, + CONSTRAINT latest_parsed_account_states_pkey PRIMARY KEY (address), + CONSTRAINT latest_parsed_account_states_address_last_tx_lt_fkey FOREIGN KEY (address, last_tx_lt) REFERENCES account_states(address, last_tx_lt) +); + +-- -- migrate to the new table +-- INSERT INTO latest_parsed_account_states (address, last_tx_lt, types, owner_address, minter_address) +-- SELECT ls.address, ls.last_tx_lt, types, owner_address, minter_address +-- FROM latest_account_states ls +-- INNER JOIN account_states s ON ls.address = s.address AND ls.last_tx_lt = s.last_tx_lt +-- WHERE array_length(s.types, 1) > 0 OR owner_address IS NOT NULL OR minter_address IS NOT NULL +-- ORDER BY s.last_tx_lt +-- LIMIT 10000; + +--bun:split +CREATE INDEX latest_parsed_account_states_types_idx ON latest_parsed_account_states USING gin (types); + +--bun:split +CREATE INDEX latest_parsed_account_states_minter_address_idx ON latest_parsed_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); + +--bun:split +CREATE INDEX latest_parsed_account_states_owner_address_idx ON latest_parsed_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql new file mode 100644 index 00000000..eded30af --- /dev/null +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -0,0 +1 @@ +DROP TABLE latest_parsed_account_states; \ No newline at end of file From 8f7704e645a585f8e4942e73d7b854d89f3a8bb6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 20 Jun 2025 22:20:44 +0300 Subject: [PATCH 065/120] [migrations] fix latest parsed account states migration --- ...183211_latest_parsed_account_states.up.sql | 28 ------------------ ...3211_latest_parsed_account_states.down.sql | 1 + ...183211_latest_parsed_account_states.up.sql | 29 ++++++++++++++++++- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql index 9adcaa3f..e69de29b 100644 --- a/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/chmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -1,28 +0,0 @@ ---bun:split -CREATE TABLE latest_parsed_account_states ( - address bytea NOT NULL, - last_tx_lt bigint NOT NULL, - types text[], - owner_address bytea, - minter_address bytea, - CONSTRAINT latest_parsed_account_states_pkey PRIMARY KEY (address), - CONSTRAINT latest_parsed_account_states_address_last_tx_lt_fkey FOREIGN KEY (address, last_tx_lt) REFERENCES account_states(address, last_tx_lt) -); - --- -- migrate to the new table --- INSERT INTO latest_parsed_account_states (address, last_tx_lt, types, owner_address, minter_address) --- SELECT ls.address, ls.last_tx_lt, types, owner_address, minter_address --- FROM latest_account_states ls --- INNER JOIN account_states s ON ls.address = s.address AND ls.last_tx_lt = s.last_tx_lt --- WHERE array_length(s.types, 1) > 0 OR owner_address IS NOT NULL OR minter_address IS NOT NULL --- ORDER BY s.last_tx_lt --- LIMIT 10000; - ---bun:split -CREATE INDEX latest_parsed_account_states_types_idx ON latest_parsed_account_states USING gin (types); - ---bun:split -CREATE INDEX latest_parsed_account_states_minter_address_idx ON latest_parsed_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); - ---bun:split -CREATE INDEX latest_parsed_account_states_owner_address_idx ON latest_parsed_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index e69de29b..eded30af 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -0,0 +1 @@ +DROP TABLE latest_parsed_account_states; \ No newline at end of file diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index eded30af..9adcaa3f 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -1 +1,28 @@ -DROP TABLE latest_parsed_account_states; \ No newline at end of file +--bun:split +CREATE TABLE latest_parsed_account_states ( + address bytea NOT NULL, + last_tx_lt bigint NOT NULL, + types text[], + owner_address bytea, + minter_address bytea, + CONSTRAINT latest_parsed_account_states_pkey PRIMARY KEY (address), + CONSTRAINT latest_parsed_account_states_address_last_tx_lt_fkey FOREIGN KEY (address, last_tx_lt) REFERENCES account_states(address, last_tx_lt) +); + +-- -- migrate to the new table +-- INSERT INTO latest_parsed_account_states (address, last_tx_lt, types, owner_address, minter_address) +-- SELECT ls.address, ls.last_tx_lt, types, owner_address, minter_address +-- FROM latest_account_states ls +-- INNER JOIN account_states s ON ls.address = s.address AND ls.last_tx_lt = s.last_tx_lt +-- WHERE array_length(s.types, 1) > 0 OR owner_address IS NOT NULL OR minter_address IS NOT NULL +-- ORDER BY s.last_tx_lt +-- LIMIT 10000; + +--bun:split +CREATE INDEX latest_parsed_account_states_types_idx ON latest_parsed_account_states USING gin (types); + +--bun:split +CREATE INDEX latest_parsed_account_states_minter_address_idx ON latest_parsed_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); + +--bun:split +CREATE INDEX latest_parsed_account_states_owner_address_idx ON latest_parsed_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); From 62cb13696e834cc210ac235d16063395cfb7e53b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 07:04:50 +0300 Subject: [PATCH 066/120] [core] remove latest_parsed_account_states table, write to latest_account_states table instead --- internal/core/account.go | 8 -- internal/core/repository/account/account.go | 103 +++++++----------- .../core/repository/account/account_test.go | 5 +- internal/core/repository/repository_test.go | 3 - ...3211_latest_parsed_account_states.down.sql | 9 +- ...183211_latest_parsed_account_states.up.sql | 95 ++++++++++++---- 6 files changed, 121 insertions(+), 102 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index fee33bd8..5a21b792 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -132,14 +132,6 @@ func (a *AccountState) BlockID() BlockID { type LatestAccountState struct { bun.BaseModel `bun:"table:latest_account_states" json:"-"` - Address addr.Address `bun:"type:bytea,pk,notnull" json:"address"` - LastTxLT uint64 `bun:"type:bigint,notnull" json:"last_tx_lt"` - AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` -} - -type LatestParsedAccountState struct { - bun.BaseModel `bun:"table:latest_parsed_account_states" json:"-"` - Address addr.Address `bun:"type:bytea,pk,notnull" json:"address"` LastTxLT uint64 `bun:"type:bigint,notnull" json:"last_tx_lt"` diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 3dee7617..eabf2a11 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -104,37 +104,6 @@ func createIndexes(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "latest account state last_tx_lt pg create index") } - // latest parsed account state - - _, err = pgDB.NewCreateIndex(). - Model(&core.LatestParsedAccountState{}). - Using("HASH"). - Column("owner_address"). - Where("owner_address IS NOT NULL"). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "latest address state owner pg create index") - } - - _, err = pgDB.NewCreateIndex(). - Model(&core.LatestParsedAccountState{}). - Using("HASH"). - Column("minter_address"). - Where("minter_address IS NOT NULL"). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "latest address state minter pg create index") - } - - _, err = pgDB.NewCreateIndex(). - Model(&core.LatestParsedAccountState{}). - Using("GIN"). - Column("types"). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "account state contract types pg create index") - } - return nil } @@ -213,15 +182,6 @@ func CreateTables(ctx context.Context, chDB *ch.DB, pgDB *bun.DB) error { return errors.Wrap(err, "latest account state pg create table") } - _, err = pgDB.NewCreateTable(). - Model(&core.LatestParsedAccountState{}). - IfNotExists(). - WithForeignKeys(). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "latest token account state pg create table") - } - return createIndexes(ctx, pgDB) } @@ -300,37 +260,22 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ for a, state := range latestStates { _, err := tx.NewInsert(). Model(&core.LatestAccountState{ - Address: a, - LastTxLT: state.LastTxLT, + Address: a, + LastTxLT: state.LastTxLT, + Types: state.Types, + OwnerAddress: state.OwnerAddress, + MinterAddress: state.MinterAddress, }). On("CONFLICT (address) DO UPDATE"). Where("latest_account_state.last_tx_lt < ?", state.LastTxLT). Set("last_tx_lt = EXCLUDED.last_tx_lt"). + Set("types = EXCLUDED.types"). + Set("owner_address = EXCLUDED.owner_address"). + Set("minter_address = EXCLUDED.minter_address"). Exec(ctx) if err != nil { return errors.Wrapf(err, "cannot set latest state for %s", &a) } - - if state.OwnerAddress != nil || state.MinterAddress != nil || len(state.Types) > 0 { - _, err := tx.NewInsert(). - Model(&core.LatestParsedAccountState{ - Address: a, - LastTxLT: state.LastTxLT, - Types: state.Types, - OwnerAddress: state.OwnerAddress, - MinterAddress: state.MinterAddress, - }). - On("CONFLICT (address) DO UPDATE"). - Where("latest_parsed_account_state.last_tx_lt < ?", state.LastTxLT). - Set("last_tx_lt = EXCLUDED.last_tx_lt"). - Set("types = EXCLUDED.types"). - Set("owner_address = EXCLUDED.owner_address"). - Set("minter_address = EXCLUDED.minter_address"). - Exec(ctx) - if err != nil { - return errors.Wrapf(err, "cannot set latest state for %s", &a) - } - } } _, err = r.ch.NewInsert().Model(&accounts).Exec(ctx) @@ -358,6 +303,12 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A return nil } + tx, err := r.pg.Begin() + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + for _, a := range accounts { for _, executions := range a.ExecutedGetMethods { sort.Slice(executions, func(i, j int) bool { return executions[i].Name < executions[j].Name }) @@ -365,7 +316,7 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A logAccountStateDataUpdate(a) - _, err := r.pg.NewUpdate().Model(a). + _, err := tx.NewUpdate().Model(a). Set("types = ?types"). Set("owner_address = ?owner_address"). Set("minter_address = ?minter_address"). @@ -382,9 +333,31 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A if err != nil { return errors.Wrapf(err, "cannot update %s acc state data", a.Address.String()) } + + _, err = tx.NewUpdate(). + Model(&core.LatestAccountState{ + Address: a.Address, + LastTxLT: a.LastTxLT, + Types: a.Types, + OwnerAddress: a.OwnerAddress, + MinterAddress: a.MinterAddress, + }). + Set("types = ?types"). + Set("owner_address = ?owner_address"). + Set("minter_address = ?minter_address"). + Where("address = ?address"). + Where("last_tx_lt = ?last_tx_lt"). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, "cannot set latest state for %s", a.Address) + } + } + + if err := tx.Commit(); err != nil { + return err } - _, err := r.ch.NewInsert().Model(&accounts).Exec(ctx) + _, err = r.ch.NewInsert().Model(&accounts).Exec(ctx) if err != nil { return err } diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index fd4fd1e7..75564b3e 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -57,10 +57,7 @@ func dropTables(t testing.TB) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - _, err := pg.NewDropTable().Model((*core.LatestParsedAccountState)(nil)).IfExists().Exec(ctx) - require.Nil(t, err) - - _, err = pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) + _, err := pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AccountStateCode)(nil)).IfExists().Exec(ctx) diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index d23eb787..cc4670d4 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -64,9 +64,6 @@ func dropTables(t testing.TB) { _, err = pg.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) require.Nil(t, err) - _, err = pg.NewDropTable().Model((*core.LatestParsedAccountState)(nil)).IfExists().Exec(ctx) - require.Nil(t, err) - _, err = pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) require.Nil(t, err) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index eded30af..17610367 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -1 +1,8 @@ -DROP TABLE latest_parsed_account_states; \ No newline at end of file +--bun:split +ALTER TABLE latest_account_states DROP COLUMN types; + +--bun:split +ALTER TABLE latest_account_states DROP COLUMN owner_address; + +--bun:split +ALTER TABLE latest_account_states DROP COLUMN minter_address; diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index 9adcaa3f..371b0658 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -1,28 +1,81 @@ --bun:split -CREATE TABLE latest_parsed_account_states ( - address bytea NOT NULL, - last_tx_lt bigint NOT NULL, - types text[], - owner_address bytea, - minter_address bytea, - CONSTRAINT latest_parsed_account_states_pkey PRIMARY KEY (address), - CONSTRAINT latest_parsed_account_states_address_last_tx_lt_fkey FOREIGN KEY (address, last_tx_lt) REFERENCES account_states(address, last_tx_lt) -); - --- -- migrate to the new table --- INSERT INTO latest_parsed_account_states (address, last_tx_lt, types, owner_address, minter_address) --- SELECT ls.address, ls.last_tx_lt, types, owner_address, minter_address --- FROM latest_account_states ls --- INNER JOIN account_states s ON ls.address = s.address AND ls.last_tx_lt = s.last_tx_lt --- WHERE array_length(s.types, 1) > 0 OR owner_address IS NOT NULL OR minter_address IS NOT NULL --- ORDER BY s.last_tx_lt --- LIMIT 10000; +ALTER TABLE latest_account_states ADD COLUMN types text[]; --bun:split -CREATE INDEX latest_parsed_account_states_types_idx ON latest_parsed_account_states USING gin (types); +ALTER TABLE latest_account_states ADD COLUMN owner_address bytea; --bun:split -CREATE INDEX latest_parsed_account_states_minter_address_idx ON latest_parsed_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); +ALTER TABLE latest_account_states ADD COLUMN minter_address bytea; + + +--bun:split +CREATE INDEX latest_account_states_types_idx ON latest_account_states USING gin (types); --bun:split -CREATE INDEX latest_parsed_account_states_owner_address_idx ON latest_parsed_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); +CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); + +--bun:split +CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); + + +-- --bun:split +-- CREATE OR REPLACE PROCEDURE batch_update_latest_account_states( +-- batch_size INT DEFAULT 10000, +-- start_from_lt BIGINT DEFAULT 0 +-- ) +-- LANGUAGE plpgsql +-- AS $$ +-- DECLARE +-- last_processed_tx_lt BIGINT := start_from_lt; +-- rows_updated INT; +-- iteration_count INT := 0; +-- max_tx_lt BIGINT; +-- BEGIN +-- RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; +-- +-- LOOP +-- -- Update the next batch +-- WITH updated AS ( +-- UPDATE latest_account_states las +-- SET +-- types = s.types, +-- owner_address = s.owner_address, +-- minter_address = s.minter_address +-- FROM account_states s +-- WHERE las.address = s.address +-- AND las.last_tx_lt = s.last_tx_lt +-- AND s.last_tx_lt >= last_processed_tx_lt +-- AND ( +-- las.types IS DISTINCT FROM s.types OR +-- las.owner_address IS DISTINCT FROM s.owner_address OR +-- las.minter_address IS DISTINCT FROM s.minter_address +-- ) +-- ORDER BY s.last_tx_lt +-- LIMIT batch_size +-- RETURNING s.last_tx_lt +-- ) +-- SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; +-- +-- -- Exit if no rows were updated +-- IF rows_updated = 0 THEN +-- RAISE NOTICE 'No more rows to update: exiting'; +-- EXIT; +-- END IF; +-- +-- -- Update the last processed tx_lt for the next iteration +-- last_processed_tx_lt := max_tx_lt; +-- iteration_count := iteration_count + 1; +-- +-- RAISE NOTICE 'Batch % complete: updated % rows, last_tx_lt = %', +-- iteration_count, rows_updated, last_processed_tx_lt; +-- +-- -- Commit after each batch +-- COMMIT; +-- END LOOP; +-- +-- RAISE NOTICE 'Batch update process completed. Total iterations: %', iteration_count; +-- END; +-- $$; +-- +-- -- Example usage: +-- -- CALL batch_update_latest_account_states(10000, 0); From 2162cf2e29731a1b1385cc564de49ab4232035bb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 09:08:37 +0300 Subject: [PATCH 067/120] [migrations] fix latest_account_states procedure for table population --- ...183211_latest_parsed_account_states.up.sql | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index 371b0658..2ce01e3b 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -26,7 +26,7 @@ CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states US -- LANGUAGE plpgsql -- AS $$ -- DECLARE --- last_processed_tx_lt BIGINT := start_from_lt; +-- last_processed_tx_lt BIGINT := start_from_lt; -- rows_updated INT; -- iteration_count INT := 0; -- max_tx_lt BIGINT; @@ -34,25 +34,34 @@ CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states US -- RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; -- -- LOOP --- -- Update the next batch --- WITH updated AS ( --- UPDATE latest_account_states las --- SET --- types = s.types, --- owner_address = s.owner_address, --- minter_address = s.minter_address +-- -- Update the next batch using a subquery to select the limited rows first +-- WITH batch_to_update AS ( +-- SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address -- FROM account_states s --- WHERE las.address = s.address --- AND las.last_tx_lt = s.last_tx_lt --- AND s.last_tx_lt >= last_processed_tx_lt +-- WHERE s.last_tx_lt >= last_processed_tx_lt -- AND ( --- las.types IS DISTINCT FROM s.types OR --- las.owner_address IS DISTINCT FROM s.owner_address OR --- las.minter_address IS DISTINCT FROM s.minter_address +-- s.types IS NOT NULL OR +-- s.owner_address IS NOT NULL OR +-- s.minter_address IS NOT NULL -- ) -- ORDER BY s.last_tx_lt -- LIMIT batch_size --- RETURNING s.last_tx_lt +-- ), +-- updated AS ( +-- UPDATE latest_account_states las +-- SET +-- types = b.types, +-- owner_address = b.owner_address, +-- minter_address = b.minter_address +-- FROM batch_to_update b +-- WHERE las.address = b.address +-- AND las.last_tx_lt = b.last_tx_lt +-- AND ( +-- las.types IS DISTINCT FROM b.types OR +-- las.owner_address IS DISTINCT FROM b.owner_address OR +-- las.minter_address IS DISTINCT FROM b.minter_address +-- ) +-- RETURNING b.last_tx_lt -- ) -- SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; -- @@ -78,4 +87,4 @@ CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states US -- $$; -- -- -- Example usage: --- -- CALL batch_update_latest_account_states(10000, 0); +-- -- CALL batch_update_latest_account_states(100000, 0); From 1549ace90f7366abaf4db1c43960169b97ec299a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 12:15:48 +0300 Subject: [PATCH 068/120] [repo] filterAccountStates: use latest_account_states for types and owner/minter filters --- internal/core/repository/account/filter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index d57a605e..e94a931b 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -129,13 +129,13 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts } if len(f.ContractTypes) > 0 { - q = q.Where(prefix+"types && ?", pgdialect.Array(f.ContractTypes)) + q = q.Where(statesTable+"types && ?", pgdialect.Array(f.ContractTypes)) } if f.OwnerAddress != nil { - q = q.Where(prefix+"owner_address = ?", f.OwnerAddress) + q = q.Where(statesTable+"owner_address = ?", f.OwnerAddress) } if f.MinterAddress != nil { - q = q.Where(prefix+"minter_address = ?", f.MinterAddress) + q = q.Where(statesTable+"minter_address = ?", f.MinterAddress) } if f.AfterTxLT != nil { From 87c99c40fa4279098f97b659e0727f2db1bbd1ec Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 13:44:07 +0300 Subject: [PATCH 069/120] [api] internalErr: print url on error --- internal/api/http/controller.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index bd0bcf30..632440c1 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -56,7 +56,11 @@ func internalErr(ctx *gin.Context, err error) { return } - log.Error().Str("path", ctx.FullPath()).Err(err).Msg("internal server error") + log.Error().Err(err). + Str("path", ctx.FullPath()). + Str("url", ctx.Request.URL.String()). + Msg("internal server error") + ctx.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } From a1b2420a80c39688609d8785a71412fd6e4dc049 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 13:54:30 +0300 Subject: [PATCH 070/120] [api] handle context cancellation properly --- internal/api/http/controller.go | 272 ++++++++++++++++---------------- 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 632440c1..983a3e9d 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -46,22 +46,22 @@ func NewController(svc app.QueryService) *Controller { return &Controller{svc: svc} } -func paramErr(ctx *gin.Context, param string, err error) { - ctx.IndentedJSON(http.StatusBadRequest, gin.H{"param": param, "error": err.Error()}) +func paramErr(c *gin.Context, param string, err error) { + c.IndentedJSON(http.StatusBadRequest, gin.H{"param": param, "error": err.Error()}) } -func internalErr(ctx *gin.Context, err error) { +func internalErr(c *gin.Context, err error) { if errors.Is(err, core.ErrInvalidArg) { - ctx.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.IndentedJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Error().Err(err). - Str("path", ctx.FullPath()). - Str("url", ctx.Request.URL.String()). + Str("path", c.FullPath()). + Str("url", c.Request.URL.String()). Msg("internal server error") - ctx.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } func unmarshalAddress(a string) (*addr.Address, error) { @@ -141,13 +141,13 @@ func getAddresses(ctx *gin.Context, name string) ([]*addr.Address, error) { // @Produce json // @Success 200 {object} aggregate.Statistics // @Router /statistics [get] -func (c *Controller) GetStatistics(ctx *gin.Context) { - ret, err := c.svc.GetStatistics(ctx) +func (ctrl *Controller) GetStatistics(c *gin.Context) { + ret, err := ctrl.svc.GetStatistics(c.Request.Context()) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } type GetInterfacesRes struct { @@ -164,13 +164,13 @@ type GetInterfacesRes struct { // @Produce json // @Success 200 {object} GetInterfacesRes // @Router /contracts/interfaces [get] -func (c *Controller) GetInterfaces(ctx *gin.Context) { - ret, err := c.svc.GetInterfaces(ctx) +func (ctrl *Controller) GetInterfaces(c *gin.Context) { + ret, err := ctrl.svc.GetInterfaces(c.Request.Context()) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, GetInterfacesRes{Total: len(ret), Results: ret}) + c.IndentedJSON(http.StatusOK, GetInterfacesRes{Total: len(ret), Results: ret}) } type GetOperationsRes struct { @@ -187,13 +187,13 @@ type GetOperationsRes struct { // @Produce json // @Success 200 {object} GetOperationsRes // @Router /contracts/operations [get] -func (c *Controller) GetOperations(ctx *gin.Context) { - ret, err := c.svc.GetOperations(ctx) +func (ctrl *Controller) GetOperations(c *gin.Context) { + ret, err := ctrl.svc.GetOperations(c.Request.Context()) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, GetOperationsRes{Total: len(ret), Results: ret}) + c.IndentedJSON(http.StatusOK, GetOperationsRes{Total: len(ret), Results: ret}) } type GetDefinitionsRes struct { @@ -210,13 +210,13 @@ type GetDefinitionsRes struct { // @Produce json // @Success 200 {object} GetDefinitionsRes // @Router /contracts/definitions [get] -func (c *Controller) GetDefinitions(ctx *gin.Context) { - ret, err := c.svc.GetDefinitions(ctx) +func (ctrl *Controller) GetDefinitions(c *gin.Context) { + ret, err := ctrl.svc.GetDefinitions(c.Request.Context()) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, GetDefinitionsRes{Total: len(ret), Results: ret}) + c.IndentedJSON(http.StatusOK, GetDefinitionsRes{Total: len(ret), Results: ret}) } // GetBlocks godoc @@ -236,20 +236,20 @@ func (c *Controller) GetDefinitions(ctx *gin.Context) { // @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.BlocksRes // @Router /blocks [get] -func (c *Controller) GetBlocks(ctx *gin.Context) { +func (ctrl *Controller) GetBlocks(c *gin.Context) { var req filter.BlocksReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "block_filter", err) + paramErr(c, "block_filter", err) return } if req.Limit > 100 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - if mw := int32(-1); ctx.Query("workchain") == "" { + if mw := int32(-1); c.Query("workchain") == "" { req.Workchain = &mw } @@ -262,17 +262,17 @@ func (c *Controller) GetBlocks(ctx *gin.Context) { req.Order, err = unmarshalSorting(req.Order) if err != nil { - paramErr(ctx, "order", err) + paramErr(c, "order", err) return } - ret, err := c.svc.FilterBlocks(ctx, &req) + ret, err := ctrl.svc.FilterBlocks(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } type GetLabelCategoriesRes struct { @@ -289,14 +289,14 @@ type GetLabelCategoriesRes struct { // @Produce json // @Success 200 {object} GetLabelCategoriesRes // @Router /labels/categories [get] -func (c *Controller) GetLabelCategories(ctx *gin.Context) { - ret, err := c.svc.GetLabelCategories(ctx) +func (ctrl *Controller) GetLabelCategories(c *gin.Context) { + ret, err := ctrl.svc.GetLabelCategories(c.Request.Context()) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, GetLabelCategoriesRes{Total: len(ret), Results: ret}) + c.IndentedJSON(http.StatusOK, GetLabelCategoriesRes{Total: len(ret), Results: ret}) } // GetLabels godoc @@ -312,26 +312,26 @@ func (c *Controller) GetLabelCategories(ctx *gin.Context) { // @Param limit query int false "limit" default(3) maximum(10000) // @Success 200 {object} filter.LabelsRes // @Router /labels [get] -func (c *Controller) GetLabels(ctx *gin.Context) { +func (ctrl *Controller) GetLabels(c *gin.Context) { var req filter.LabelsReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "label_filter", err) + paramErr(c, "label_filter", err) return } if req.Limit > 10000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - ret, err := c.svc.FilterLabels(ctx, &req) + ret, err := ctrl.svc.FilterLabels(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // GetAccounts godoc @@ -352,48 +352,48 @@ func (c *Controller) GetLabels(ctx *gin.Context) { // @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.AccountsRes // @Router /accounts [get] -func (c *Controller) GetAccounts(ctx *gin.Context) { +func (ctrl *Controller) GetAccounts(c *gin.Context) { req := filter.AccountsReq{WithCodeData: true} - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "account_filter", err) + paramErr(c, "account_filter", err) return } if req.Limit > 10000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - req.Addresses, err = getAddresses(ctx, "address") + req.Addresses, err = getAddresses(c, "address") if err != nil { - paramErr(ctx, "address", err) + paramErr(c, "address", err) return } - req.OwnerAddress, err = unmarshalAddress(ctx.Query("owner_address")) + req.OwnerAddress, err = unmarshalAddress(c.Query("owner_address")) if err != nil { - paramErr(ctx, "owner_address", err) + paramErr(c, "owner_address", err) return } - req.MinterAddress, err = unmarshalAddress(ctx.Query("minter_address")) + req.MinterAddress, err = unmarshalAddress(c.Query("minter_address")) if err != nil { - paramErr(ctx, "minter_address", err) + paramErr(c, "minter_address", err) return } req.Order, err = unmarshalSorting(req.Order) if err != nil { - paramErr(ctx, "order", err) + paramErr(c, "order", err) return } - ret, err := c.svc.FilterAccounts(ctx, &req) + ret, err := ctrl.svc.FilterAccounts(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // AggregateAccounts godoc @@ -408,38 +408,38 @@ func (c *Controller) GetAccounts(ctx *gin.Context) { // @Param limit query int false "limit" default(25) maximum(1000000) // @Success 200 {object} aggregate.AccountsRes // @Router /accounts/aggregated [get] -func (c *Controller) AggregateAccounts(ctx *gin.Context) { +func (ctrl *Controller) AggregateAccounts(c *gin.Context) { var req aggregate.AccountsReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "account_filter", err) + paramErr(c, "account_filter", err) return } if req.Limit > 1000000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - req.Address, err = unmarshalAddress(ctx.Query("address")) + req.Address, err = unmarshalAddress(c.Query("address")) if err != nil { - paramErr(ctx, "address", err) + paramErr(c, "address", err) return } - req.MinterAddress, err = unmarshalAddress(ctx.Query("minter_address")) + req.MinterAddress, err = unmarshalAddress(c.Query("minter_address")) if err != nil { - paramErr(ctx, "minter_address", err) + paramErr(c, "minter_address", err) return } - ret, err := c.svc.AggregateAccounts(ctx, &req) + ret, err := ctrl.svc.AggregateAccounts(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // AggregateAccountsHistory godoc @@ -457,28 +457,28 @@ func (c *Controller) AggregateAccounts(ctx *gin.Context) { // @Param interval query string true "group interval" Enums(24h, 8h, 4h, 1h, 15m) // @Success 200 {object} history.AccountsRes // @Router /accounts/aggregated/history [get] -func (c *Controller) AggregateAccountsHistory(ctx *gin.Context) { +func (ctrl *Controller) AggregateAccountsHistory(c *gin.Context) { var req history.AccountsReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "account_filter", err) + paramErr(c, "account_filter", err) return } - req.MinterAddress, err = unmarshalAddress(ctx.Query("minter_address")) + req.MinterAddress, err = unmarshalAddress(c.Query("minter_address")) if err != nil { - paramErr(ctx, "minter_address", err) + paramErr(c, "minter_address", err) return } - ret, err := c.svc.AggregateAccountsHistory(ctx, &req) + ret, err := ctrl.svc.AggregateAccountsHistory(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // GetTransactions godoc @@ -499,51 +499,51 @@ func (c *Controller) AggregateAccountsHistory(ctx *gin.Context) { // @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.TransactionsRes // @Router /transactions [get] -func (c *Controller) GetTransactions(ctx *gin.Context) { +func (ctrl *Controller) GetTransactions(c *gin.Context) { var req filter.TransactionsReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "tx_filter", err) + paramErr(c, "tx_filter", err) return } if req.Limit > 10000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - req.Hash, err = unmarshalBytes(ctx.Query("hash")) + req.Hash, err = unmarshalBytes(c.Query("hash")) if err != nil { - paramErr(ctx, "hash", err) + paramErr(c, "hash", err) return } - req.InMsgHash, err = unmarshalBytes(ctx.Query("in_msg_hash")) + req.InMsgHash, err = unmarshalBytes(c.Query("in_msg_hash")) if err != nil { - paramErr(ctx, "in_msg_hash", err) + paramErr(c, "in_msg_hash", err) return } req.WithAccountState = true req.WithMessages = true - req.Addresses, err = getAddresses(ctx, "address") + req.Addresses, err = getAddresses(c, "address") if err != nil { - paramErr(ctx, "address", err) + paramErr(c, "address", err) return } req.Order, err = unmarshalSorting(req.Order) if err != nil { - paramErr(ctx, "order", err) + paramErr(c, "order", err) return } - ret, err := c.svc.FilterTransactions(ctx, &req) + ret, err := ctrl.svc.FilterTransactions(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // AggregateTransactionsHistory godoc @@ -561,28 +561,28 @@ func (c *Controller) GetTransactions(ctx *gin.Context) { // @Param interval query string true "group interval" Enums(24h, 8h, 4h, 1h, 15m) // @Success 200 {object} history.TransactionsRes // @Router /transactions/aggregated/history [get] -func (c *Controller) AggregateTransactionsHistory(ctx *gin.Context) { +func (ctrl *Controller) AggregateTransactionsHistory(c *gin.Context) { var req history.TransactionsReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "tx_filter", err) + paramErr(c, "tx_filter", err) return } - req.Addresses, err = getAddresses(ctx, "address") + req.Addresses, err = getAddresses(c, "address") if err != nil { - paramErr(ctx, "address", err) + paramErr(c, "address", err) return } - ret, err := c.svc.AggregateTransactionsHistory(ctx, &req) + ret, err := ctrl.svc.AggregateTransactionsHistory(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // GetMessages godoc @@ -607,39 +607,39 @@ func (c *Controller) AggregateTransactionsHistory(ctx *gin.Context) { // @Param count query bool false "count total number of rows" default(false) // @Success 200 {object} filter.MessagesRes // @Router /messages [get] -func (c *Controller) GetMessages(ctx *gin.Context) { +func (ctrl *Controller) GetMessages(c *gin.Context) { var req filter.MessagesReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "msg_filter", err) + paramErr(c, "msg_filter", err) return } if req.Limit > 10000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - req.Hash, err = unmarshalBytes(ctx.Query("hash")) + req.Hash, err = unmarshalBytes(c.Query("hash")) if err != nil { - paramErr(ctx, "hash", err) + paramErr(c, "hash", err) return } - req.SrcAddresses, err = getAddresses(ctx, "src_address") + req.SrcAddresses, err = getAddresses(c, "src_address") if err != nil { - paramErr(ctx, "src_address", err) + paramErr(c, "src_address", err) return } - req.DstAddresses, err = getAddresses(ctx, "dst_address") + req.DstAddresses, err = getAddresses(c, "dst_address") if err != nil { - paramErr(ctx, "dst_address", err) + paramErr(c, "dst_address", err) return } - if op := ctx.Query("operation_id"); op != "" { + if op := c.Query("operation_id"); op != "" { id, err := unmarshalOperationID(op) if err != nil { - paramErr(ctx, "operation_id", err) + paramErr(c, "operation_id", err) return } req.OperationID = &id @@ -647,16 +647,16 @@ func (c *Controller) GetMessages(ctx *gin.Context) { req.Order, err = unmarshalSorting(req.Order) if err != nil { - paramErr(ctx, "order", err) + paramErr(c, "order", err) return } - ret, err := c.svc.FilterMessages(ctx, &req) + ret, err := ctrl.svc.FilterMessages(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // AggregateMessages godoc @@ -673,39 +673,39 @@ func (c *Controller) GetMessages(ctx *gin.Context) { // @Param limit query int false "limit" default(25) maximum(1000000) // @Success 200 {object} aggregate.MessagesRes // @Router /messages/aggregated [get] -func (c *Controller) AggregateMessages(ctx *gin.Context) { +func (ctrl *Controller) AggregateMessages(c *gin.Context) { var req aggregate.MessagesReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "msg_filter", err) + paramErr(c, "msg_filter", err) return } if req.Limit > 1000000 { - paramErr(ctx, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) + paramErr(c, "limit", errors.Wrapf(core.ErrInvalidArg, "limit is too big")) return } - req.Address, err = unmarshalAddress(ctx.Query("address")) + req.Address, err = unmarshalAddress(c.Query("address")) if err != nil { - paramErr(ctx, "address", err) + paramErr(c, "address", err) return } switch req.OrderBy { case "amount", "count": default: - paramErr(ctx, "order_by", errors.Wrap(core.ErrInvalidArg, "wrong order_by argument")) + paramErr(c, "order_by", errors.Wrap(core.ErrInvalidArg, "wrong order_by argument")) return } - ret, err := c.svc.AggregateMessages(ctx, &req) + ret, err := ctrl.svc.AggregateMessages(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } // AggregateMessagesHistory godoc @@ -729,36 +729,36 @@ func (c *Controller) AggregateMessages(ctx *gin.Context) { // @Param interval query string true "group interval" Enums(24h, 8h, 4h, 1h, 15m) // @Success 200 {object} history.MessagesRes // @Router /messages/aggregated/history [get] -func (c *Controller) AggregateMessagesHistory(ctx *gin.Context) { +func (ctrl *Controller) AggregateMessagesHistory(c *gin.Context) { var req history.MessagesReq - err := ctx.ShouldBindQuery(&req) + err := c.ShouldBindQuery(&req) if err != nil { - paramErr(ctx, "msg_filter", err) + paramErr(c, "msg_filter", err) return } - req.SrcAddresses, err = getAddresses(ctx, "src_address") + req.SrcAddresses, err = getAddresses(c, "src_address") if err != nil { - paramErr(ctx, "src_address", err) + paramErr(c, "src_address", err) return } - req.DstAddresses, err = getAddresses(ctx, "dst_address") + req.DstAddresses, err = getAddresses(c, "dst_address") if err != nil { - paramErr(ctx, "dst_address", err) + paramErr(c, "dst_address", err) return } - req.MinterAddress, err = unmarshalAddress(ctx.Query("minter_address")) + req.MinterAddress, err = unmarshalAddress(c.Query("minter_address")) if err != nil { - paramErr(ctx, "minter_address", err) + paramErr(c, "minter_address", err) return } - ret, err := c.svc.AggregateMessagesHistory(ctx, &req) + ret, err := ctrl.svc.AggregateMessagesHistory(c.Request.Context(), &req) if err != nil { - internalErr(ctx, err) + internalErr(c, err) return } - ctx.IndentedJSON(http.StatusOK, ret) + c.IndentedJSON(http.StatusOK, ret) } From 3f0d63c4bf828c0b238092c7de8a743343c21785 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 14:25:04 +0300 Subject: [PATCH 071/120] [core] latest_account_states: add created_lt --- internal/core/account.go | 2 + internal/core/repository/account/account.go | 17 ++- ..._latest_account_states_created_lt.down.sql | 0 ...11_latest_account_states_created_lt.up.sql | 0 ...3211_latest_parsed_account_states.down.sql | 4 + ...183211_latest_parsed_account_states.up.sql | 140 +++++++++--------- ..._latest_account_states_created_lt.down.sql | 4 + ...11_latest_account_states_created_lt.up.sql | 62 ++++++++ 8 files changed, 152 insertions(+), 77 deletions(-) create mode 100644 migrations/chmigrations/20250621110511_latest_account_states_created_lt.down.sql create mode 100644 migrations/chmigrations/20250621110511_latest_account_states_created_lt.up.sql create mode 100644 migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql create mode 100644 migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql diff --git a/internal/core/account.go b/internal/core/account.go index 5a21b792..6dcee148 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -140,6 +140,8 @@ type LatestAccountState struct { OwnerAddress *addr.Address `bun:"type:bytea" json:"owner_address,omitempty"` // universal column for many contracts MinterAddress *addr.Address `bun:"type:bytea" json:"minter_address,omitempty"` + CreatedLT uint64 `bun:"type:bigint,notnull" json:"created_lt"` + AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index eabf2a11..dcb5425c 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -258,14 +258,17 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ } for a, state := range latestStates { + latest := core.LatestAccountState{ + Address: a, + LastTxLT: state.LastTxLT, + Types: state.Types, + OwnerAddress: state.OwnerAddress, + MinterAddress: state.MinterAddress, + CreatedLT: state.LastTxLT, // it is being written only on insert + } + _, err := tx.NewInsert(). - Model(&core.LatestAccountState{ - Address: a, - LastTxLT: state.LastTxLT, - Types: state.Types, - OwnerAddress: state.OwnerAddress, - MinterAddress: state.MinterAddress, - }). + Model(&latest). On("CONFLICT (address) DO UPDATE"). Where("latest_account_state.last_tx_lt < ?", state.LastTxLT). Set("last_tx_lt = EXCLUDED.last_tx_lt"). diff --git a/migrations/chmigrations/20250621110511_latest_account_states_created_lt.down.sql b/migrations/chmigrations/20250621110511_latest_account_states_created_lt.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/chmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/chmigrations/20250621110511_latest_account_states_created_lt.up.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index 17610367..795e6660 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -1,3 +1,7 @@ +--bun:split +DROP PROCEDURE batch_update_latest_parsed_account_states; + + --bun:split ALTER TABLE latest_account_states DROP COLUMN types; diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index 2ce01e3b..fd69e45f 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -18,73 +18,73 @@ CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states U CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); --- --bun:split --- CREATE OR REPLACE PROCEDURE batch_update_latest_account_states( --- batch_size INT DEFAULT 10000, --- start_from_lt BIGINT DEFAULT 0 --- ) --- LANGUAGE plpgsql --- AS $$ --- DECLARE --- last_processed_tx_lt BIGINT := start_from_lt; --- rows_updated INT; --- iteration_count INT := 0; --- max_tx_lt BIGINT; --- BEGIN --- RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; --- --- LOOP --- -- Update the next batch using a subquery to select the limited rows first --- WITH batch_to_update AS ( --- SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address --- FROM account_states s --- WHERE s.last_tx_lt >= last_processed_tx_lt --- AND ( --- s.types IS NOT NULL OR --- s.owner_address IS NOT NULL OR --- s.minter_address IS NOT NULL --- ) --- ORDER BY s.last_tx_lt --- LIMIT batch_size --- ), --- updated AS ( --- UPDATE latest_account_states las --- SET --- types = b.types, --- owner_address = b.owner_address, --- minter_address = b.minter_address --- FROM batch_to_update b --- WHERE las.address = b.address --- AND las.last_tx_lt = b.last_tx_lt --- AND ( --- las.types IS DISTINCT FROM b.types OR --- las.owner_address IS DISTINCT FROM b.owner_address OR --- las.minter_address IS DISTINCT FROM b.minter_address --- ) --- RETURNING b.last_tx_lt --- ) --- SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; --- --- -- Exit if no rows were updated --- IF rows_updated = 0 THEN --- RAISE NOTICE 'No more rows to update: exiting'; --- EXIT; --- END IF; --- --- -- Update the last processed tx_lt for the next iteration --- last_processed_tx_lt := max_tx_lt; --- iteration_count := iteration_count + 1; --- --- RAISE NOTICE 'Batch % complete: updated % rows, last_tx_lt = %', --- iteration_count, rows_updated, last_processed_tx_lt; --- --- -- Commit after each batch --- COMMIT; --- END LOOP; --- --- RAISE NOTICE 'Batch update process completed. Total iterations: %', iteration_count; --- END; --- $$; --- --- -- Example usage: --- -- CALL batch_update_latest_account_states(100000, 0); +--bun:split +CREATE OR REPLACE PROCEDURE batch_update_latest_parsed_account_states( + batch_size INT DEFAULT 10000, + start_from_lt BIGINT DEFAULT 0 +) +LANGUAGE plpgsql +AS $$ +DECLARE + last_processed_tx_lt BIGINT := start_from_lt; + rows_updated INT; + iteration_count INT := 0; + max_tx_lt BIGINT; +BEGIN + RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; + + LOOP + -- Update the next batch using a subquery to select the limited rows first + WITH batch_to_update AS ( + SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address + FROM account_states s + WHERE s.last_tx_lt >= last_processed_tx_lt + AND ( + s.types IS NOT NULL OR + s.owner_address IS NOT NULL OR + s.minter_address IS NOT NULL + ) + ORDER BY s.last_tx_lt + LIMIT batch_size + ), + updated AS ( + UPDATE latest_account_states las + SET + types = b.types, + owner_address = b.owner_address, + minter_address = b.minter_address + FROM batch_to_update b + WHERE las.address = b.address + AND las.last_tx_lt = b.last_tx_lt + AND ( + las.types IS DISTINCT FROM b.types OR + las.owner_address IS DISTINCT FROM b.owner_address OR + las.minter_address IS DISTINCT FROM b.minter_address + ) + RETURNING b.last_tx_lt + ) + SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; + + -- Exit if no rows were updated + IF rows_updated = 0 THEN + RAISE NOTICE 'No more rows to update: exiting'; + EXIT; + END IF; + + -- Update the last processed tx_lt for the next iteration + last_processed_tx_lt := max_tx_lt; + iteration_count := iteration_count + 1; + + RAISE NOTICE 'Batch % complete: updated % rows, last_tx_lt = %', + iteration_count, rows_updated, last_processed_tx_lt; + + -- Commit after each batch + COMMIT; + END LOOP; + + RAISE NOTICE 'Batch update process completed. Total iterations: %', iteration_count; +END; +$$; + +-- Example usage: +-- CALL batch_update_latest_account_states(100000, 0); diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql new file mode 100644 index 00000000..6b832af6 --- /dev/null +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql @@ -0,0 +1,4 @@ +--bun:split +DROP PROCEDURE batch_fill_account_states_created_lt; + +ALTER TABLE latest_account_states DROP COLUMN created_lt bigint; diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql new file mode 100644 index 00000000..698854f3 --- /dev/null +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -0,0 +1,62 @@ +--bun:split +ALTER TABLE latest_account_states ADD COLUMN created_lt bigint; + + +--bun:split +CREATE OR REPLACE PROCEDURE batch_fill_account_states_created_lt( + batch_size INT DEFAULT 10000, + start_from_address BYTEA DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +DECLARE + last_processed_address BYTEA := start_from_address; + rows_updated INT; + iteration_count INT := 0; + max_address BYTEA; +BEGIN + RAISE NOTICE 'Starting batch fill of created_lt with batch size: %', batch_size; + + LOOP + -- Directly query account_states for minimum last_tx_lt per address + WITH min_tx_lt AS ( + SELECT address, MIN(last_tx_lt) as min_lt + FROM account_states + WHERE (last_processed_address IS NULL OR address > last_processed_address) + GROUP BY address + ORDER BY address + LIMIT batch_size + ), + updated AS ( + UPDATE latest_account_states las + SET created_lt = m.min_lt + FROM min_tx_lt m + WHERE las.address = m.address + AND las.created_lt IS NULL + RETURNING las.address + ) + SELECT COUNT(*), MAX(address) INTO rows_updated, max_address FROM updated; + + -- Exit if no rows were updated + IF rows_updated = 0 THEN + RAISE NOTICE 'No more rows to update: exiting'; + EXIT; + END IF; + + -- Update the last processed address for the next iteration + last_processed_address := max_address; + iteration_count := iteration_count + 1; + + RAISE NOTICE 'Batch % complete: updated % rows, last address = %', + iteration_count, rows_updated, encode(last_processed_address, 'hex'); + + -- Commit after each batch + COMMIT; + END LOOP; + + RAISE NOTICE 'Batch fill process completed. Total iterations: %', iteration_count; +END; +$$; + +-- Example usage: +-- CALL batch_fill_account_states_created_lt(10000); From 79339f73dfbd54f77bd5ddc9e412e17658de487a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 14:27:27 +0300 Subject: [PATCH 072/120] [migrations] comment out procedures --- ...3211_latest_parsed_account_states.down.sql | 4 +- ...183211_latest_parsed_account_states.up.sql | 140 +++++++++--------- ..._latest_account_states_created_lt.down.sql | 4 +- ...11_latest_account_states_created_lt.up.sql | 116 +++++++-------- 4 files changed, 132 insertions(+), 132 deletions(-) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index 795e6660..800a4062 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -1,5 +1,5 @@ ---bun:split -DROP PROCEDURE batch_update_latest_parsed_account_states; +-- --bun:split +-- DROP PROCEDURE batch_update_latest_parsed_account_states; --bun:split diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index fd69e45f..5f47aa09 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -18,73 +18,73 @@ CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states U CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); ---bun:split -CREATE OR REPLACE PROCEDURE batch_update_latest_parsed_account_states( - batch_size INT DEFAULT 10000, - start_from_lt BIGINT DEFAULT 0 -) -LANGUAGE plpgsql -AS $$ -DECLARE - last_processed_tx_lt BIGINT := start_from_lt; - rows_updated INT; - iteration_count INT := 0; - max_tx_lt BIGINT; -BEGIN - RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; - - LOOP - -- Update the next batch using a subquery to select the limited rows first - WITH batch_to_update AS ( - SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address - FROM account_states s - WHERE s.last_tx_lt >= last_processed_tx_lt - AND ( - s.types IS NOT NULL OR - s.owner_address IS NOT NULL OR - s.minter_address IS NOT NULL - ) - ORDER BY s.last_tx_lt - LIMIT batch_size - ), - updated AS ( - UPDATE latest_account_states las - SET - types = b.types, - owner_address = b.owner_address, - minter_address = b.minter_address - FROM batch_to_update b - WHERE las.address = b.address - AND las.last_tx_lt = b.last_tx_lt - AND ( - las.types IS DISTINCT FROM b.types OR - las.owner_address IS DISTINCT FROM b.owner_address OR - las.minter_address IS DISTINCT FROM b.minter_address - ) - RETURNING b.last_tx_lt - ) - SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; - - -- Exit if no rows were updated - IF rows_updated = 0 THEN - RAISE NOTICE 'No more rows to update: exiting'; - EXIT; - END IF; - - -- Update the last processed tx_lt for the next iteration - last_processed_tx_lt := max_tx_lt; - iteration_count := iteration_count + 1; - - RAISE NOTICE 'Batch % complete: updated % rows, last_tx_lt = %', - iteration_count, rows_updated, last_processed_tx_lt; - - -- Commit after each batch - COMMIT; - END LOOP; - - RAISE NOTICE 'Batch update process completed. Total iterations: %', iteration_count; -END; -$$; - --- Example usage: --- CALL batch_update_latest_account_states(100000, 0); +-- --bun:split +-- CREATE OR REPLACE PROCEDURE batch_update_latest_parsed_account_states( +-- batch_size INT DEFAULT 10000, +-- start_from_lt BIGINT DEFAULT 0 +-- ) +-- LANGUAGE plpgsql +-- AS $$ +-- DECLARE +-- last_processed_tx_lt BIGINT := start_from_lt; +-- rows_updated INT; +-- iteration_count INT := 0; +-- max_tx_lt BIGINT; +-- BEGIN +-- RAISE NOTICE 'Starting batch update process with batch size: %', batch_size; +-- +-- LOOP +-- -- Update the next batch using a subquery to select the limited rows first +-- WITH batch_to_update AS ( +-- SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address +-- FROM account_states s +-- WHERE s.last_tx_lt >= last_processed_tx_lt +-- AND ( +-- s.types IS NOT NULL OR +-- s.owner_address IS NOT NULL OR +-- s.minter_address IS NOT NULL +-- ) +-- ORDER BY s.last_tx_lt +-- LIMIT batch_size +-- ), +-- updated AS ( +-- UPDATE latest_account_states las +-- SET +-- types = b.types, +-- owner_address = b.owner_address, +-- minter_address = b.minter_address +-- FROM batch_to_update b +-- WHERE las.address = b.address +-- AND las.last_tx_lt = b.last_tx_lt +-- AND ( +-- las.types IS DISTINCT FROM b.types OR +-- las.owner_address IS DISTINCT FROM b.owner_address OR +-- las.minter_address IS DISTINCT FROM b.minter_address +-- ) +-- RETURNING b.last_tx_lt +-- ) +-- SELECT COUNT(*), MAX(last_tx_lt) INTO rows_updated, max_tx_lt FROM updated; +-- +-- -- Exit if no rows were updated +-- IF rows_updated = 0 THEN +-- RAISE NOTICE 'No more rows to update: exiting'; +-- EXIT; +-- END IF; +-- +-- -- Update the last processed tx_lt for the next iteration +-- last_processed_tx_lt := max_tx_lt; +-- iteration_count := iteration_count + 1; +-- +-- RAISE NOTICE 'Batch % complete: updated % rows, last_tx_lt = %', +-- iteration_count, rows_updated, last_processed_tx_lt; +-- +-- -- Commit after each batch +-- COMMIT; +-- END LOOP; +-- +-- RAISE NOTICE 'Batch update process completed. Total iterations: %', iteration_count; +-- END; +-- $$; +-- +-- -- Example usage: +-- -- CALL batch_update_latest_account_states(100000, 0); diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql index 6b832af6..d6c48667 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql @@ -1,4 +1,4 @@ ---bun:split -DROP PROCEDURE batch_fill_account_states_created_lt; +-- --bun:split +-- DROP PROCEDURE batch_fill_account_states_created_lt; ALTER TABLE latest_account_states DROP COLUMN created_lt bigint; diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql index 698854f3..a20ce98d 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -2,61 +2,61 @@ ALTER TABLE latest_account_states ADD COLUMN created_lt bigint; ---bun:split -CREATE OR REPLACE PROCEDURE batch_fill_account_states_created_lt( - batch_size INT DEFAULT 10000, - start_from_address BYTEA DEFAULT NULL -) -LANGUAGE plpgsql -AS $$ -DECLARE - last_processed_address BYTEA := start_from_address; - rows_updated INT; - iteration_count INT := 0; - max_address BYTEA; -BEGIN - RAISE NOTICE 'Starting batch fill of created_lt with batch size: %', batch_size; - - LOOP - -- Directly query account_states for minimum last_tx_lt per address - WITH min_tx_lt AS ( - SELECT address, MIN(last_tx_lt) as min_lt - FROM account_states - WHERE (last_processed_address IS NULL OR address > last_processed_address) - GROUP BY address - ORDER BY address - LIMIT batch_size - ), - updated AS ( - UPDATE latest_account_states las - SET created_lt = m.min_lt - FROM min_tx_lt m - WHERE las.address = m.address - AND las.created_lt IS NULL - RETURNING las.address - ) - SELECT COUNT(*), MAX(address) INTO rows_updated, max_address FROM updated; - - -- Exit if no rows were updated - IF rows_updated = 0 THEN - RAISE NOTICE 'No more rows to update: exiting'; - EXIT; - END IF; - - -- Update the last processed address for the next iteration - last_processed_address := max_address; - iteration_count := iteration_count + 1; - - RAISE NOTICE 'Batch % complete: updated % rows, last address = %', - iteration_count, rows_updated, encode(last_processed_address, 'hex'); - - -- Commit after each batch - COMMIT; - END LOOP; - - RAISE NOTICE 'Batch fill process completed. Total iterations: %', iteration_count; -END; -$$; - --- Example usage: --- CALL batch_fill_account_states_created_lt(10000); +-- --bun:split +-- CREATE OR REPLACE PROCEDURE batch_fill_account_states_created_lt( +-- batch_size INT DEFAULT 10000, +-- start_from_address BYTEA DEFAULT NULL +-- ) +-- LANGUAGE plpgsql +-- AS $$ +-- DECLARE +-- last_processed_address BYTEA := start_from_address; +-- rows_updated INT; +-- iteration_count INT := 0; +-- max_address BYTEA; +-- BEGIN +-- RAISE NOTICE 'Starting batch fill of created_lt with batch size: %', batch_size; +-- +-- LOOP +-- -- Directly query account_states for minimum last_tx_lt per address +-- WITH min_tx_lt AS ( +-- SELECT address, MIN(last_tx_lt) as min_lt +-- FROM account_states +-- WHERE (last_processed_address IS NULL OR address > last_processed_address) +-- GROUP BY address +-- ORDER BY address +-- LIMIT batch_size +-- ), +-- updated AS ( +-- UPDATE latest_account_states las +-- SET created_lt = m.min_lt +-- FROM min_tx_lt m +-- WHERE las.address = m.address +-- AND las.created_lt IS NULL +-- RETURNING las.address +-- ) +-- SELECT COUNT(*), MAX(address) INTO rows_updated, max_address FROM updated; +-- +-- -- Exit if no rows were updated +-- IF rows_updated = 0 THEN +-- RAISE NOTICE 'No more rows to update: exiting'; +-- EXIT; +-- END IF; +-- +-- -- Update the last processed address for the next iteration +-- last_processed_address := max_address; +-- iteration_count := iteration_count + 1; +-- +-- RAISE NOTICE 'Batch % complete: updated % rows, last address = %', +-- iteration_count, rows_updated, encode(last_processed_address, 'hex'); +-- +-- -- Commit after each batch +-- COMMIT; +-- END LOOP; +-- +-- RAISE NOTICE 'Batch fill process completed. Total iterations: %', iteration_count; +-- END; +-- $$; +-- +-- -- Example usage: +-- -- CALL batch_fill_account_states_created_lt(10000); From 8dd969bc2e13e9399ea928cf0f5cd09c14d7607d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 14:53:49 +0300 Subject: [PATCH 073/120] [migrations] account_states created_lt: fix procedure --- ...50621110511_latest_account_states_created_lt.up.sql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql index a20ce98d..8c6294a2 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -34,11 +34,17 @@ ALTER TABLE latest_account_states ADD COLUMN created_lt bigint; -- WHERE las.address = m.address -- AND las.created_lt IS NULL -- RETURNING las.address +-- ), +-- last_address AS ( +-- SELECT address FROM updated ORDER BY address DESC LIMIT 1 +-- ), +-- address_count AS ( +-- SELECT COUNT(*) AS count FROM updated -- ) --- SELECT COUNT(*), MAX(address) INTO rows_updated, max_address FROM updated; +-- SELECT address_count.count, last_address.address INTO rows_updated, max_address FROM address_count, last_address; -- -- -- Exit if no rows were updated --- IF rows_updated = 0 THEN +-- IF COALESCE(rows_updated, 0) = 0 THEN -- RAISE NOTICE 'No more rows to update: exiting'; -- EXIT; -- END IF; From 40ca1b1316d491bcd30a9500e508f6850b121575 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 15:35:58 +0300 Subject: [PATCH 074/120] [repo] countMsgFullScan: handle empty messages table and unfiltered max created lt --- internal/core/repository/msg/filter.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index b14ed4db..28695054 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -79,13 +79,13 @@ func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (re func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesReq) (count int, maxLt uint64, err error) { var result struct { Count int - MaxLT uint64 `ch:"max_lt"` + MaxLT *uint64 `ch:"max_lt"` } q := r.ch.NewSelect(). Model((*core.Message)(nil)). ColumnExpr("count(*) AS count"). - ColumnExpr("max(created_lt) AS max_lt") + ColumnExpr("(SELECT max(created_lt) FROM messages) AS max_lt") // unfiltered max if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) @@ -119,7 +119,11 @@ func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesR return 0, 0, err } - return result.Count, result.MaxLT, nil + if result.MaxLT == nil { + return 0, 0, core.ErrNotFound + } + + return result.Count, *result.MaxLT, nil } func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.MessagesReq, startLt uint64) (partialCount int, maxLt uint64, err error) { @@ -150,6 +154,9 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int if err != nil { return 0, err } + if errors.Is(err, core.ErrNotFound) { + return 0, nil + } if err := r.messagesFilterCache.Set(req.MessagesFilter, count, maxLT); err != nil { return 0, err } From 8edec356c22db2dfce73668a389b207e616883d3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 15:46:46 +0300 Subject: [PATCH 075/120] [repo] countMsgPartialScan: unfiltered max created lt and handle case with no new rows --- internal/core/repository/msg/filter.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 28695054..20da36c7 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -135,7 +135,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag q := r.pg.NewSelect(). Model((*core.Message)(nil)). ColumnExpr("count(*) AS count"). - ColumnExpr("max(created_lt) AS max_lt"). + ColumnExpr("(select max(created_lt) from messages where created_lt > ?) AS max_lt", startLt). // unfiltered max Where("created_lt > ?", startLt) q = r.getFilterMessageQuery(q, &req.MessagesFilter) @@ -144,6 +144,10 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag return 0, 0, err } + if result.MaxLT == 0 { + result.MaxLT = startLt // no new rows + } + return result.Count, result.MaxLT, nil } From e300b311abd8a4198fa2999c552b07a2110ea110 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 18:46:31 +0300 Subject: [PATCH 076/120] [repo] account filter: cache counting, do full scan on types/owner/minter filter --- internal/core/repository/account/account.go | 15 +- internal/core/repository/account/filter.go | 155 +++++++++++++++++--- internal/core/repository/msg/filter.go | 6 +- internal/core/repository/msg/msg.go | 12 +- 4 files changed, 158 insertions(+), 30 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index dcb5425c..7ae98951 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -8,6 +8,7 @@ import ( "reflect" "sort" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -18,18 +19,26 @@ import ( "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" "github.com/tonindexer/anton/internal/core/repository" ) var _ repository.Account = (*Repository)(nil) type Repository struct { - ch *ch.DB - pg *bun.DB + ch *ch.DB + pg *bun.DB + statesFilterCountCache *filter.Cache + latestStatesFilterCountCache *filter.Cache } func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { - return &Repository{ch: ck, pg: pg} + return &Repository{ + ch: ck, + pg: pg, + statesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + latestStatesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + } } func createIndexes(ctx context.Context, pgDB *bun.DB) error { diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index e94a931b..6f3b92a1 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -171,14 +171,36 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts return ret, err } -func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsReq) (count int, err error) { - q := r.ch.NewSelect().Model((*core.AccountState)(nil)) +func (r *Repository) countAccountStatesFullScan(ctx context.Context, f *filter.AccountsReq) (count int, maxLt uint64, err error) { + if f.LatestState && (len(f.ContractTypes) > 0 || f.MinterAddress != nil || f.OwnerAddress != nil) { + return 0, 0, errors.New("clickhouse latest account states full scan is not supported for these filters") + } + + var result struct { + Count int + MaxLT *uint64 `ch:"max_lt"` + } + + var q *ch.SelectQuery + if f.LatestState { + // For latest account states, we need to count distinct addresses + q = r.ch.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("count(distinct address) AS count"). + ColumnExpr("(SELECT max(last_tx_lt) FROM account_states) AS max_lt") // unfiltered max + } else { + // For historical account states, we count all records + q = r.ch.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("(SELECT max(last_tx_lt) FROM account_states) AS max_lt") // unfiltered max + } if len(f.Addresses) > 0 { q = q.Where("address in (?)", ch.In(f.Addresses)) } if len(f.StateIDs) > 0 { - return 0, errors.Wrap(core.ErrNotImplemented, "do not count on filter by account state ids") + return 0, 0, errors.Wrap(core.ErrNotImplemented, "do not count on filter by account state ids") } if f.Workchain != nil { @@ -200,27 +222,124 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR if f.MinterAddress != nil { q = q.Where("minter_address = ?", f.MinterAddress) } - if f.OwnerAddress != nil { - if f.LatestState { - q = r.ch.NewSelect().TableExpr("(?) as q", // because owner address can change - q.Column("address"). - ColumnExpr("argMax(owner_address, last_tx_lt) as owner_address"). - Group("address")). - Where("owner_address = ?", f.OwnerAddress) - } else { - q = q.Where("owner_address = ?", f.OwnerAddress) - } + q = q.Where("owner_address = ?", f.OwnerAddress) } - if f.LatestState { - q = q.ColumnExpr("count(distinct address)") + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + if result.MaxLT == nil { + return 0, 0, core.ErrNotFound + } + + return result.Count, *result.MaxLT, nil +} + +func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *filter.AccountsReq, startLt uint64) (partialCount int, maxLt uint64, err error) { + var result struct { + Count int + MaxLT uint64 `bun:"max_lt"` + } + + var q *bun.SelectQuery + if req.LatestState { + q = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("(select max(last_tx_lt) from latest_account_states) AS max_lt") + if startLt > 0 { + q = q.Where("created_lt > ?", startLt) + } } else { - q = q.ColumnExpr("count(*)") + q = r.pg.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("(select max(last_tx_lt) from account_states) AS max_lt"). + Where("last_tx_lt > ?", startLt) + } + + if len(req.Addresses) > 0 { + q = q.Where("address in (?)", bun.In(req.Addresses)) + } + + if !req.LatestState { + if req.Workchain != nil { + q = q.Where("workchain = ?", *req.Workchain) + } + if req.Shard != nil { + q = q.Where("shard = ?", *req.Shard) + } + if req.BlockSeqNoLeq != nil { + q = q.Where("block_seq_no <= ?", *req.BlockSeqNoLeq) + } + if req.BlockSeqNoBeq != nil { + q = q.Where("block_seq_no >= ?", *req.BlockSeqNoBeq) + } + } + + if len(req.ContractTypes) > 0 { + q = q.Where("types && ?", pgdialect.Array(req.ContractTypes)) + } + if req.OwnerAddress != nil { + q = q.Where("owner_address = ?", req.OwnerAddress) + } + if req.MinterAddress != nil { + q = q.Where("minter_address = ?", req.MinterAddress) + } + + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + if result.MaxLT == 0 { + result.MaxLT = startLt // no new rows + } + + return result.Count, result.MaxLT, nil +} + +func (r *Repository) countAccountStates(ctx context.Context, req *filter.AccountsReq) (int, error) { + if req.LatestState && (len(req.ContractTypes) > 0 || req.OwnerAddress != nil || req.MinterAddress != nil) { + count, _, err := r.countAccountStatesPartialScan(ctx, req, 0) + return count, err + } + + // choose the appropriate cache based on whether we're querying latest or historical states + cache := r.statesFilterCountCache + if req.LatestState { + cache = r.latestStatesFilterCountCache + } + + // try to get from cache + count, maxLT, err := cache.Get(req.AccountsFilter) + if errors.Is(err, core.ErrNotFound) { + // full scan for initial count + count, maxLT, err = r.countAccountStatesFullScan(ctx, req) + if err != nil { + return 0, err + } + if errors.Is(err, core.ErrNotFound) { + return 0, nil + } + if err := cache.Set(req.AccountsFilter, count, maxLT); err != nil { + return 0, err + } + } else if err != nil { + return 0, err + } + + // get partial count since last cached value + partialCount, maxLT, err := r.countAccountStatesPartialScan(ctx, req, maxLT) + if err != nil { + return 0, err + } + if err := cache.Set(req.AccountsFilter, count+partialCount, maxLT); err != nil { + return 0, err } - err = q.Scan(ctx, &count) - return count, err + return count + partialCount, nil } func (r *Repository) getCodeData(ctx context.Context, rows []*core.AccountState, excludeCode, excludeData bool) error { //nolint:gocognit,gocyclo // TODO: make one function working for both code and data diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 20da36c7..ff8d3e9d 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -152,7 +152,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag } func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int, error) { - count, maxLT, err := r.messagesFilterCache.Get(req.MessagesFilter) + count, maxLT, err := r.messagesFilterCountCache.Get(req.MessagesFilter) if errors.Is(err, core.ErrNotFound) { count, maxLT, err = r.countMsgFullScan(ctx, req) if err != nil { @@ -161,7 +161,7 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int if errors.Is(err, core.ErrNotFound) { return 0, nil } - if err := r.messagesFilterCache.Set(req.MessagesFilter, count, maxLT); err != nil { + if err := r.messagesFilterCountCache.Set(req.MessagesFilter, count, maxLT); err != nil { return 0, err } } @@ -173,7 +173,7 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int if err != nil { return 0, err } - if err := r.messagesFilterCache.Set(req.MessagesFilter, count+partialCount, maxLT); err != nil { + if err := r.messagesFilterCountCache.Set(req.MessagesFilter, count+partialCount, maxLT); err != nil { return 0, err } diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index fd773032..1edb4cd6 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -21,16 +21,16 @@ import ( var _ repository.Message = (*Repository)(nil) type Repository struct { - ch *ch.DB - pg *bun.DB - messagesFilterCache *filter.Cache + ch *ch.DB + pg *bun.DB + messagesFilterCountCache *filter.Cache } func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ - ch: ck, - pg: pg, - messagesFilterCache: filter.NewCache(7 * 24 * time.Hour), + ch: ck, + pg: pg, + messagesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), } } From 45ce9d3ff8d2b163b6b269490eb2e37b7912f8b2 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 19:15:26 +0300 Subject: [PATCH 077/120] [repo] countAccountStates / countMsg: handle full scan not found error properly --- internal/core/repository/account/filter.go | 6 +++--- internal/core/repository/msg/filter.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 6f3b92a1..f3d6e056 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -317,12 +317,12 @@ func (r *Repository) countAccountStates(ctx context.Context, req *filter.Account if errors.Is(err, core.ErrNotFound) { // full scan for initial count count, maxLT, err = r.countAccountStatesFullScan(ctx, req) - if err != nil { - return 0, err - } if errors.Is(err, core.ErrNotFound) { return 0, nil } + if err != nil { + return 0, err + } if err := cache.Set(req.AccountsFilter, count, maxLT); err != nil { return 0, err } diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index ff8d3e9d..dbc08539 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -155,12 +155,12 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int count, maxLT, err := r.messagesFilterCountCache.Get(req.MessagesFilter) if errors.Is(err, core.ErrNotFound) { count, maxLT, err = r.countMsgFullScan(ctx, req) - if err != nil { - return 0, err - } if errors.Is(err, core.ErrNotFound) { return 0, nil } + if err != nil { + return 0, err + } if err := r.messagesFilterCountCache.Set(req.MessagesFilter, count, maxLT); err != nil { return 0, err } From e66ab927cd215259a38917c96de4141fce12d5ac Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 21 Jun 2025 19:16:06 +0300 Subject: [PATCH 078/120] [repo] transaction: cache counting --- internal/core/filter/tx.go | 4 +- internal/core/repository/tx/filter.go | 118 +++++++++++++++++++++----- internal/core/repository/tx/tx.go | 13 ++- 3 files changed, 110 insertions(+), 25 deletions(-) diff --git a/internal/core/filter/tx.go b/internal/core/filter/tx.go index ee629983..08863a64 100644 --- a/internal/core/filter/tx.go +++ b/internal/core/filter/tx.go @@ -16,6 +16,8 @@ type TransactionsFilter struct { Workchain *int32 `form:"workchain"` BlockID *core.BlockID + + CreatedLT *uint64 `form:"created_lt"` } type TransactionsReq struct { @@ -28,8 +30,6 @@ type TransactionsReq struct { Order string `form:"order"` // ASC, DESC - CreatedLT *uint64 `form:"created_lt"` - AfterTxLT *uint64 `form:"after"` Limit int `form:"limit"` Count bool `form:"count"` diff --git a/internal/core/repository/tx/filter.go b/internal/core/repository/tx/filter.go index eb0aadf9..bfd4df24 100644 --- a/internal/core/repository/tx/filter.go +++ b/internal/core/repository/tx/filter.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/pkg/errors" "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" @@ -11,23 +12,7 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (r *Repository) filterTx(ctx context.Context, req *filter.TransactionsReq) (ret []*core.Transaction, err error) { - q := r.pg.NewSelect().Model(&ret) - - if req.WithAccountState { - q = q.Relation("Account", func(q *bun.SelectQuery) *bun.SelectQuery { - if len(req.ExcludeColumn) > 0 { - q = q.ExcludeColumn(req.ExcludeColumn...) - } - return q - }) - } - if req.WithMessages { - q = q. - Relation("InMsg"). - Relation("OutMsg") - } - +func (r *Repository) getFilterTxQuery(q *bun.SelectQuery, req *filter.TransactionsFilter) *bun.SelectQuery { if len(req.Hash) > 0 { q = q.Where("transaction.hash = ?", req.Hash) } @@ -48,6 +33,27 @@ func (r *Repository) filterTx(ctx context.Context, req *filter.TransactionsReq) if req.CreatedLT != nil { q = q.Where("transaction.created_lt = ?", *req.CreatedLT) } + return q +} + +func (r *Repository) filterTx(ctx context.Context, req *filter.TransactionsReq) (ret []*core.Transaction, err error) { + q := r.pg.NewSelect().Model(&ret) + + if req.WithAccountState { + q = q.Relation("Account", func(q *bun.SelectQuery) *bun.SelectQuery { + if len(req.ExcludeColumn) > 0 { + q = q.ExcludeColumn(req.ExcludeColumn...) + } + return q + }) + } + if req.WithMessages { + q = q. + Relation("InMsg"). + Relation("OutMsg") + } + + q = r.getFilterTxQuery(q, &req.TransactionsFilter) if req.AfterTxLT != nil { if req.Order == "ASC" { @@ -70,9 +76,16 @@ func (r *Repository) filterTx(ctx context.Context, req *filter.TransactionsReq) return ret, err } -func (r *Repository) countTx(ctx context.Context, req *filter.TransactionsReq) (int, error) { +func (r *Repository) countTxFullScan(ctx context.Context, req *filter.TransactionsReq) (count int, maxLt uint64, err error) { + var result struct { + Count int + MaxLT *uint64 `ch:"max_lt"` + } + q := r.ch.NewSelect(). - Model((*core.Transaction)(nil)) + Model((*core.Transaction)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("(SELECT max(created_lt) FROM transactions) AS max_lt") // unfiltered max if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) @@ -95,7 +108,72 @@ func (r *Repository) countTx(ctx context.Context, req *filter.TransactionsReq) ( q = q.Where("created_lt = ?", *req.CreatedLT) } - return q.Count(ctx) + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + if result.MaxLT == nil { + return 0, 0, core.ErrNotFound + } + + return result.Count, *result.MaxLT, nil +} + +func (r *Repository) countTxPartialScan(ctx context.Context, req *filter.TransactionsReq, startLt uint64) (partialCount int, maxLt uint64, err error) { + var result struct { + Count int + MaxLT uint64 `bun:"max_lt"` + } + + q := r.pg.NewSelect(). + Model((*core.Transaction)(nil)). + ColumnExpr("count(*) AS count"). + ColumnExpr("COALESCE(max(created_lt), ?) AS max_lt", startLt). + Where("created_lt > ?", startLt) + + q = r.getFilterTxQuery(q, &req.TransactionsFilter) + + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, err + } + + if result.MaxLT == 0 { + result.MaxLT = startLt // no new rows + } + + return result.Count, result.MaxLT, nil +} + +func (r *Repository) countTx(ctx context.Context, req *filter.TransactionsReq) (int, error) { + count, maxLT, err := r.transactionsFilterCountCache.Get(req.TransactionsFilter) + if errors.Is(err, core.ErrNotFound) { + count, maxLT, err = r.countTxFullScan(ctx, req) + if err != nil { + return 0, err + } + if errors.Is(err, core.ErrNotFound) { + return 0, nil + } + if err := r.transactionsFilterCountCache.Set(req.TransactionsFilter, count, maxLT); err != nil { + return 0, err + } + } else if err != nil { + return 0, err + } + + if len(req.Hash) > 0 || req.BlockID != nil || req.CreatedLT != nil { + return count, nil // count value cannot change on any of these filters + } + + partialCount, maxLT, err := r.countTxPartialScan(ctx, req, maxLT) + if err != nil { + return 0, err + } + if err := r.transactionsFilterCountCache.Set(req.TransactionsFilter, count+partialCount, maxLT); err != nil { + return 0, err + } + + return count + partialCount, nil } func (r *Repository) FilterTransactions(ctx context.Context, req *filter.TransactionsReq) (*filter.TransactionsRes, error) { diff --git a/internal/core/repository/tx/tx.go b/internal/core/repository/tx/tx.go index 43eea430..aed155d6 100644 --- a/internal/core/repository/tx/tx.go +++ b/internal/core/repository/tx/tx.go @@ -2,24 +2,31 @@ package tx import ( "context" + "time" "github.com/pkg/errors" "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" "github.com/tonindexer/anton/internal/core/repository" ) var _ repository.Transaction = (*Repository)(nil) type Repository struct { - ch *ch.DB - pg *bun.DB + ch *ch.DB + pg *bun.DB + transactionsFilterCountCache *filter.Cache } func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { - return &Repository{ch: ck, pg: pg} + return &Repository{ + ch: ck, + pg: pg, + transactionsFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + } } func createIndexes(ctx context.Context, pgDB *bun.DB) error { From b1f50bd5653e46b24686b033ee6f5eee7ca98f1a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 12:42:55 +0300 Subject: [PATCH 079/120] [core] convert SkipAddress function to SkippedAddresses map --- internal/app/query/query.go | 2 +- internal/core/account.go | 99 ++++++++++++++----------------------- 2 files changed, 37 insertions(+), 64 deletions(-) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index 2cb1fd9a..1235f77c 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -144,7 +144,7 @@ func (s *Service) fetchSkippedAccounts(ctx context.Context, req *filter.Accounts if found[a] { continue } - if core.SkipAddress(a) { + if core.SkippedAddresses[a] { // fetch heavy skipped account states skipped = append(skipped, a) continue diff --git a/internal/core/account.go b/internal/core/account.go index 6dcee148..93019e18 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -145,69 +145,42 @@ type LatestAccountState struct { AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` } -func SkipAddress(a addr.Address) bool { - switch a.Base64() { - case "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c": // burn address - return true - case "Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU": // system contract - return true - case "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF": // elector contract - return true - case "Ef80UXx731GHxVr0-LYf3DIViMerdo3uJLAG3ykQZFjXz2kW": // log tests contract - return true - case "Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn": // config contract - return true - case "EQAHI1vGuw7d4WG-CtfDrWqEPNtmUuKjKFEFeJmZaqqfWTvW": // BSC Bridge Collector - return true - case "EQCuzvIOXLjH2tv35gY4tzhIvXCqZWDuK9kUhFGXKLImgxT5": // ETH Bridge Collector - return true - case "EQA2u5Z5Fn59EUvTI-TIrX8PIGKQzNj3qLixdCPPujfJleXC", - "EQA2Pnxp0rMB9L6SU2z1VqfMIFIfutiTjQWFEXnwa_zPh0P3", - "EQDhIloDu1FWY9WFAgQDgw0RjuT5bLkf15Rmd5LCG3-0hyoe": // strange heavy testnet address - return true - case "EQAWBIxrfQDExJSfFmE5UL1r9drse0dQx_eaV8w9S77VK32F": // tongo emulator segmentation fault - return true - case "EQCnBscEi-KGfqJ5Wk6R83yrqtmUum94SXnSDz3AOQfHGjDw", - "EQA9xJgsYbsTjWxEcaxv8DLW3iRJtHzjwFzFAEWVxup0WH0R": // quackquack (?) - return true - case "EQCqNjAPkigLdS5gxHiHitWuzF3ZN-gX7MlX4Qfy2cGS3FWx": // ton-squid - return true - case "EQCp6qUScSUYB66ExDIlla8kfnUpP5cLZ_zhy4nlOPC-fqFo": // highload wallet v2 with heavy data - return true - case "EQC1Bq1GJY9ON_2WpSroVlXpejzfLNA8XoL2MYxtN50ZbJfN": // TryTON - return true - case "EQCTsnUmD2wvN-SBaa7CMF1sgTfC-YNywqbdPepKw34VBglS": // TryTON NFT collection - return true - case "EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh": // TON lama jetton distribution - return true - case "EQBvc1QLuqTMx0NNTZ4DD__UzfTvkEOJMs67XoZhHVihWtMN": // POO jetton distribution - return true - case "EQDF6fj6ydJJX_ArwxINjP-0H8zx982W4XgbkKzGvceUWvXl": // ETH Token Bridge Collector - return true - case "EQC_0ScHnb7bVoyInXLkZ2G4XRHg97S9XrPKCUDaO1ZRyFhZ": // Gemz Checkin - return true - case "EQD_QUnVTBzwG-8GCkqnQ4xiWxU0oPZn9Pon_rq0MZVdIBuf", - "EQB2MfIcTbwtshE8VOv0YA6ZWpb9bbj79D_SUXHZYv04X47c": // Wonton (?) - return true - case "EQAqk4SStGaodBsjW0zc8H4psrsx258cCdqw4Nm3ScnMYpLf": // some service (?) - return true - case "EQDlHrYvmV9R91wNbqvpzo-_pXu4Q6vQZo0-t2CplC6Zgh4y": // RBT trader - return true - case "EQD5iFPj0zk1mA-GatG_3QtBNWVzuRKatszH1MUYAw6aVeK2": // some service claims (?) - return true - case "EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7", - "EQAlMRLTYOoG6kM0d3dLHqgK30ol3qIYwMNtEelktzXP_pD5", - "EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK", - "EQAU35_2hAbymisgUrhGa4bIJUtEJjVNVS7zBrqfKaENd67N", - "EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ", - "EQDCR0XQ0qNQJNjITRpo59mFsP0pjx81ImtXx92mJBnIc7m4", - "EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC", - "EQD-r3joXyZ2kWRxraqze6ypKoVtSx1qlKlJsNEjyLM7ujs7", - "EQDTCD85dI5Cu8O1eDecuARaagwaOPMacnXwqn8KB0-1DN8P": // unknown - return true - default: - return false - } +var SkippedAddresses = map[addr.Address]bool{ + *addr.MustFromBase64("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"): true, // burn address + *addr.MustFromBase64("Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU"): true, // system contract + *addr.MustFromBase64("Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF"): true, // elector contract + *addr.MustFromBase64("Ef80UXx731GHxVr0-LYf3DIViMerdo3uJLAG3ykQZFjXz2kW"): true, // log tests contract + *addr.MustFromBase64("Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn"): true, // config contract + *addr.MustFromBase64("EQAHI1vGuw7d4WG-CtfDrWqEPNtmUuKjKFEFeJmZaqqfWTvW"): true, // BSC Bridge Collector + *addr.MustFromBase64("EQCuzvIOXLjH2tv35gY4tzhIvXCqZWDuK9kUhFGXKLImgxT5"): true, // ETH Bridge Collector + *addr.MustFromBase64("EQA2u5Z5Fn59EUvTI-TIrX8PIGKQzNj3qLixdCPPujfJleXC"): true, // strange heavy testnet address + *addr.MustFromBase64("EQA2Pnxp0rMB9L6SU2z1VqfMIFIfutiTjQWFEXnwa_zPh0P3"): true, // strange heavy testnet address + *addr.MustFromBase64("EQDhIloDu1FWY9WFAgQDgw0RjuT5bLkf15Rmd5LCG3-0hyoe"): true, // strange heavy testnet address + *addr.MustFromBase64("EQAWBIxrfQDExJSfFmE5UL1r9drse0dQx_eaV8w9S77VK32F"): true, // tongo emulator segmentation fault + *addr.MustFromBase64("EQCnBscEi-KGfqJ5Wk6R83yrqtmUum94SXnSDz3AOQfHGjDw"): true, // quackquack (?) + *addr.MustFromBase64("EQA9xJgsYbsTjWxEcaxv8DLW3iRJtHzjwFzFAEWVxup0WH0R"): true, // quackquack (?) + *addr.MustFromBase64("EQCqNjAPkigLdS5gxHiHitWuzF3ZN-gX7MlX4Qfy2cGS3FWx"): true, // ton-squid + *addr.MustFromBase64("EQCp6qUScSUYB66ExDIlla8kfnUpP5cLZ_zhy4nlOPC-fqFo"): true, // highload wallet v2 with heavy data + *addr.MustFromBase64("EQC1Bq1GJY9ON_2WpSroVlXpejzfLNA8XoL2MYxtN50ZbJfN"): true, // TryTON + *addr.MustFromBase64("EQCTsnUmD2wvN-SBaa7CMF1sgTfC-YNywqbdPepKw34VBglS"): true, // TryTON NFT collection + *addr.MustFromBase64("EQCatS3EvWAhYaFEmLK_rOWViVgzN9RrHYh_PpNQ01X_WTPh"): true, // TON lama jetton distribution + *addr.MustFromBase64("EQBvc1QLuqTMx0NNTZ4DD__UzfTvkEOJMs67XoZhHVihWtMN"): true, // POO jetton distribution + *addr.MustFromBase64("EQDF6fj6ydJJX_ArwxINjP-0H8zx982W4XgbkKzGvceUWvXl"): true, // ETH Token Bridge Collector + *addr.MustFromBase64("EQC_0ScHnb7bVoyInXLkZ2G4XRHg97S9XrPKCUDaO1ZRyFhZ"): true, // Gemz Checkin + *addr.MustFromBase64("EQD_QUnVTBzwG-8GCkqnQ4xiWxU0oPZn9Pon_rq0MZVdIBuf"): true, // Wonton (?) + *addr.MustFromBase64("EQB2MfIcTbwtshE8VOv0YA6ZWpb9bbj79D_SUXHZYv04X47c"): true, // Wonton (?) + *addr.MustFromBase64("EQAqk4SStGaodBsjW0zc8H4psrsx258cCdqw4Nm3ScnMYpLf"): true, // some service (?) + *addr.MustFromBase64("EQDlHrYvmV9R91wNbqvpzo-_pXu4Q6vQZo0-t2CplC6Zgh4y"): true, // RBT trader + *addr.MustFromBase64("EQD5iFPj0zk1mA-GatG_3QtBNWVzuRKatszH1MUYAw6aVeK2"): true, // some service claims (?) + *addr.MustFromBase64("EQCfrctTcgYp6cd2iqgAVKiLKauJvBNC4sc84xYBvspyw3q7"): true, + *addr.MustFromBase64("EQAlMRLTYOoG6kM0d3dLHqgK30ol3qIYwMNtEelktzXP_pD5"): true, + *addr.MustFromBase64("EQDa5wUCdTj1tqYV-LyIcefBHd3IGacvzhcBrSjmlKY2xnaK"): true, + *addr.MustFromBase64("EQAU35_2hAbymisgUrhGa4bIJUtEJjVNVS7zBrqfKaENd67N"): true, + *addr.MustFromBase64("EQCxr1o-x7cEFb3vALiYMOW7QPuAoGHMtw1Yab5m6HrnuIuZ"): true, + *addr.MustFromBase64("EQDCR0XQ0qNQJNjITRpo59mFsP0pjx81ImtXx92mJBnIc7m4"): true, + *addr.MustFromBase64("EQAYNJOQTA9FqZF4QGxzcPEvvMWkP76snfI7gATCur_86psC"): true, + *addr.MustFromBase64("EQD-r3joXyZ2kWRxraqze6ypKoVtSx1qlKlJsNEjyLM7ujs7"): true, + *addr.MustFromBase64("EQDTCD85dI5Cu8O1eDecuARaagwaOPMacnXwqn8KB0-1DN8P"): true, } type AccountRepository interface { From 6aa631d5cfcc4a99c90415b99ed784a77ee9b20d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 12:43:42 +0300 Subject: [PATCH 080/120] [fetcher] simplify getAccount --- internal/app/fetcher/account.go | 107 +++++++++++++++----------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index d6957144..cc76a56c 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -94,8 +94,56 @@ func (s *Service) makeGetOtherAccountFunc(master *ton.BlockIDExt, lastLT uint64) return getOtherAccountFunc } +func (s *Service) getAccountUnlocked(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { + raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) + if err != nil { + return nil, errors.Wrapf(err, "get account") + } + + acc := MapAccount(b, raw) + + if raw.Code != nil { //nolint:nestif // getting get-method hashes from the library + libs, err := s.getAccountLibraries(ctx, a, raw) + if err != nil { + return acc, errors.Wrapf(err, "get account libraries") + } + if libs != nil { + acc.Libraries = libs.ToBOC() + } + + if raw.Code.GetType() == cell.LibraryCellType { + hash, err := getLibraryHash(raw.Code) + if err != nil { + return acc, errors.Wrap(err, "get library hash") + } + + lib := s.libraries.get(hash) + if lib != nil && lib.Lib != nil { + acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) + } + } else { + acc.GetMethodHashes, _ = abi.GetMethodHashes(raw.Code) + } + } + + if acc.Status == core.NonExist { + return acc, errors.Wrap(core.ErrNotFound, "account does not exists") + } + + // sometimes, to parse the full account data we need to get other contracts states + // for example, to get nft item data + getOtherAccount := s.makeGetOtherAccountFunc(master, acc.LastTxLT) + + err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) + if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + return acc, errors.Wrapf(err, "parse account data (%s)", acc.Address.String()) + } + + return acc, nil +} + func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { - if core.SkipAddress(a) { + if core.SkippedAddresses[a] { return nil, errors.Wrap(core.ErrNotFound, "skip account") } @@ -117,62 +165,9 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a lock.Do(func() { defer core.Timer(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) - var ( - acc *core.AccountState - err error - ) - defer func() { s.accBlockStatesCache.Put(stateID, getAccountRes{acc: acc, err: err}) }() - - raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) - if err != nil { - err = errors.Wrapf(err, "get account") - return - } - - acc = MapAccount(b, raw) - - if raw.Code != nil { //nolint:nestif // getting get-method hashes from the library - libs, getErr := s.getAccountLibraries(ctx, a, raw) - if getErr != nil { - err = errors.Wrapf(getErr, "get account libraries") - return - } - if libs != nil { - acc.Libraries = libs.ToBOC() - } - - if raw.Code.GetType() == cell.LibraryCellType { - hash, getErr := getLibraryHash(raw.Code) - if getErr != nil { - err = errors.Wrap(getErr, "get library hash") - return - } - - lib := s.libraries.get(hash) - if lib != nil && lib.Lib != nil { - acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) - } - } else { - acc.GetMethodHashes, _ = abi.GetMethodHashes(raw.Code) - } - } - - if acc.Status == core.NonExist { - err = errors.Wrap(core.ErrNotFound, "account does not exists") - return - } - - // sometimes, to parse the full account data we need to get other contracts states - // for example, to get nft item data - getOtherAccount := s.makeGetOtherAccountFunc(master, acc.LastTxLT) - - err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) - if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { - err = errors.Wrapf(err, "parse account data (%s)", acc.Address.String()) - return - } + acc, err := s.getAccountUnlocked(ctx, master, b, a) - err = nil + s.accBlockStatesCache.Put(stateID, getAccountRes{acc: acc, err: err}) }) res, ok = s.accBlockStatesCache.Get(stateID) From c9825d40fa2978f1aeba6550aa1cf96cca77d545 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 12:44:07 +0300 Subject: [PATCH 081/120] some simple linter fixes --- cmd/db/db.go | 4 +-- internal/app/query/stats.go | 29 +++++++++------------- internal/core/repository/account/filter.go | 2 +- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index e68995bc..923d762e 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -27,7 +27,7 @@ import ( "github.com/tonindexer/anton/migrations/pgmigrations" ) -func newMigrators() (pg *migrate.Migrator, ch *chmigrate.Migrator, err error) { +func newMigrators() (pg *migrate.Migrator, ck *chmigrate.Migrator, err error) { chURL := env.GetString("DB_CH_URL", "") pgURL := env.GetString("DB_PG_URL", "") @@ -678,7 +678,7 @@ var Command = &cli.Command{ return nil } - getCodeData := func(ctx context.Context, rows []*core.AccountState) error { //nolint:gocognit,gocyclo // TODO: make one function working for both code and data + getCodeData := func(ctx context.Context, rows []*core.AccountState) error { //nolint:gocyclo // TODO: make one function working for both code and data codeHashesSet, dataHashesSet := map[string]struct{}{}, map[string]struct{}{} for _, row := range rows { if len(row.CodeHash) == 32 { diff --git a/internal/app/query/stats.go b/internal/app/query/stats.go index d17deb2d..03bd75c5 100644 --- a/internal/app/query/stats.go +++ b/internal/app/query/stats.go @@ -22,21 +22,18 @@ func (s *Service) updateStatsLoop() { ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() - for { - select { - case <-ticker.C: - if !s.running() { - return - } - - s.mx.RLock() - lastUpdate := s.statsUpdateTs - lastTry := s.statsFailTs - s.mx.RUnlock() - - if time.Since(lastUpdate) > statsUpdateDelay && time.Since(lastTry) > statsRetryDelay { - s.updateStats() - } + for range ticker.C { + if !s.running() { + return + } + + s.mx.RLock() + lastUpdate := s.statsUpdateTs + lastTry := s.statsFailTs + s.mx.RUnlock() + + if time.Since(lastUpdate) > statsUpdateDelay && time.Since(lastTry) > statsRetryDelay { + s.updateStats() } } } @@ -58,6 +55,4 @@ func (s *Service) updateStats() { s.statsCached = stats s.statsUpdateTs = time.Now() - - return } diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index f3d6e056..17e49278 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -84,7 +84,7 @@ func flattenStateIDs(ids []*core.AccountStateID) (ret [][]any) { return } -func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq) (ret []*core.AccountState, err error) { //nolint:gocyclo,gocognit // that's ok +func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq) (ret []*core.AccountState, err error) { //nolint:gocognit // that's ok var ( q *bun.SelectQuery prefix, statesTable string From acdea88ef3a4b30588a07695544e0f3c37aa49ee Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 12:47:29 +0300 Subject: [PATCH 082/120] remove nolint directives --- cmd/db/db.go | 2 +- internal/core/repository/account/filter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index 923d762e..ee34aed9 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -678,7 +678,7 @@ var Command = &cli.Command{ return nil } - getCodeData := func(ctx context.Context, rows []*core.AccountState) error { //nolint:gocyclo // TODO: make one function working for both code and data + getCodeData := func(ctx context.Context, rows []*core.AccountState) error { codeHashesSet, dataHashesSet := map[string]struct{}{}, map[string]struct{}{} for _, row := range rows { if len(row.CodeHash) == 32 { diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 17e49278..09611fda 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -84,7 +84,7 @@ func flattenStateIDs(ids []*core.AccountStateID) (ret [][]any) { return } -func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq) (ret []*core.AccountState, err error) { //nolint:gocognit // that's ok +func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq) (ret []*core.AccountState, err error) { var ( q *bun.SelectQuery prefix, statesTable string From cbdca81c1e460f7de2f96b040d87d18b939aa3e3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 13:13:20 +0300 Subject: [PATCH 083/120] [migration] add db transactions --- ...3211_latest_parsed_account_states.down.sql | 14 +++++------ ...183211_latest_parsed_account_states.up.sql | 25 +++++++------------ ..._latest_account_states_created_lt.down.sql | 7 ++++-- ...11_latest_account_states_created_lt.up.sql | 8 ++++-- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index 800a4062..c9507505 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -1,12 +1,10 @@ --- --bun:split -- DROP PROCEDURE batch_update_latest_parsed_account_states; +BEGIN; ---bun:split -ALTER TABLE latest_account_states DROP COLUMN types; + ALTER TABLE latest_account_states + DROP COLUMN types, + DROP COLUMN owner_address, + DROP COLUMN minter_address; ---bun:split -ALTER TABLE latest_account_states DROP COLUMN owner_address; - ---bun:split -ALTER TABLE latest_account_states DROP COLUMN minter_address; +COMMIT; diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index 5f47aa09..d27469fd 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -1,22 +1,15 @@ ---bun:split -ALTER TABLE latest_account_states ADD COLUMN types text[]; +BEGIN; ---bun:split -ALTER TABLE latest_account_states ADD COLUMN owner_address bytea; + ALTER TABLE latest_account_states + ADD COLUMN types text[], + ADD COLUMN owner_address bytea, + ADD COLUMN minter_address bytea; ---bun:split -ALTER TABLE latest_account_states ADD COLUMN minter_address bytea; - - ---bun:split -CREATE INDEX latest_account_states_types_idx ON latest_account_states USING gin (types); - ---bun:split -CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); - ---bun:split -CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); + CREATE INDEX latest_account_states_types_idx ON latest_account_states USING gin (types); + CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); + CREATE INDEX latest_account_states_owner_address_idx ON latest_account_states USING btree (owner_address) WHERE (owner_address IS NOT NULL); +COMMIT; -- --bun:split -- CREATE OR REPLACE PROCEDURE batch_update_latest_parsed_account_states( diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql index d6c48667..4832755d 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.down.sql @@ -1,4 +1,7 @@ --- --bun:split -- DROP PROCEDURE batch_fill_account_states_created_lt; -ALTER TABLE latest_account_states DROP COLUMN created_lt bigint; +BEGIN; + + ALTER TABLE latest_account_states DROP COLUMN created_lt bigint; + +COMMIT; diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql index 8c6294a2..d60ffb8a 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -1,5 +1,9 @@ ---bun:split -ALTER TABLE latest_account_states ADD COLUMN created_lt bigint; +BEGIN; + + ALTER TABLE latest_account_states + ADD COLUMN created_lt bigint; + +COMMIT; -- --bun:split From c2f547f0ea9dd5269064cb35574af2e1dbc5b989 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 14:05:53 +0300 Subject: [PATCH 084/120] [repo] tune filter cache ttl --- internal/core/repository/account/account.go | 4 ++-- internal/core/repository/msg/msg.go | 2 +- internal/core/repository/tx/tx.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 7ae98951..ce452159 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -36,8 +36,8 @@ func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ ch: ck, pg: pg, - statesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), - latestStatesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + statesFilterCountCache: filter.NewCache(24 * time.Hour), + latestStatesFilterCountCache: filter.NewCache(24 * time.Hour), } } diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 1edb4cd6..d5e105b0 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -30,7 +30,7 @@ func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ ch: ck, pg: pg, - messagesFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + messagesFilterCountCache: filter.NewCache(4 * time.Hour), } } diff --git a/internal/core/repository/tx/tx.go b/internal/core/repository/tx/tx.go index aed155d6..9edfc19a 100644 --- a/internal/core/repository/tx/tx.go +++ b/internal/core/repository/tx/tx.go @@ -25,7 +25,7 @@ func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ ch: ck, pg: pg, - transactionsFilterCountCache: filter.NewCache(7 * 24 * time.Hour), + transactionsFilterCountCache: filter.NewCache(24 * time.Hour), } } From 1d6db34b639d904f653703477dbef658ded3d544 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 14:34:18 +0300 Subject: [PATCH 085/120] [repo] tune messages filter cache ttl --- internal/core/repository/msg/msg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index d5e105b0..f2cf91dc 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -30,7 +30,7 @@ func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ ch: ck, pg: pg, - messagesFilterCountCache: filter.NewCache(4 * time.Hour), + messagesFilterCountCache: filter.NewCache(24 * time.Hour), } } From 900653e47a8f37661fe8e22b0c3a6ebdf568987c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 17:36:06 +0300 Subject: [PATCH 086/120] [repo] messages filter counting: round created_lt for cache --- internal/core/repository/msg/filter.go | 86 +++++++++++++++++++------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index dbc08539..91a587e4 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -78,14 +78,13 @@ func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (re func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesReq) (count int, maxLt uint64, err error) { var result struct { - Count int - MaxLT *uint64 `ch:"max_lt"` + MaxLT uint64 `ch:"max_lt_value"` + RoundedMaxLT uint64 `ch:"max_lt_rounded"` + Count int } q := r.ch.NewSelect(). - Model((*core.Message)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(SELECT max(created_lt) FROM messages) AS max_lt") // unfiltered max + Model((*core.Message)(nil)) if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) @@ -115,40 +114,79 @@ func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesR q = q.Where("operation_name IN (?)", ch.In(req.OperationNames)) } + q = r.ch.NewSelect(). + With( + "max_lt", + r.ch.NewSelect(). + Model((*core.Message)(nil)). + ColumnExpr("max(created_lt) AS v"), + ). + With( + "rounded_count", + q. // query with filters + Table("max_lt"). + ColumnExpr("count(*) as v"). + Where("created_lt <= floor(max_lt.v, -7)"), // we round LT as messages in new blocks can have lower LT + ). + Table("max_lt", "rounded_count"). + ColumnExpr("max_lt.v AS max_lt_value"). + ColumnExpr("floor(max_lt.v, -7) as max_lt_rounded"). + ColumnExpr("rounded_count.v AS count") + if err := q.Scan(ctx, &result); err != nil { return 0, 0, err } - if result.MaxLT == nil { + if result.MaxLT == 0 { return 0, 0, core.ErrNotFound } - return result.Count, *result.MaxLT, nil + return result.Count, result.RoundedMaxLT, nil } -func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.MessagesReq, startLt uint64) (partialCount int, maxLt uint64, err error) { +func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.MessagesReq, startLt uint64) (partialCount, roundedCount int, roundedMaxLt uint64, err error) { var result struct { - Count int - MaxLT uint64 `ch:"max_lt"` + Since int `bun:"since_rounded_count"` + Until int `bun:"until_rounded_count"` + RoundedMaxLT uint64 `bun:"rounded_max_lt_value"` } q := r.pg.NewSelect(). - Model((*core.Message)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(select max(created_lt) from messages where created_lt > ?) AS max_lt", startLt). // unfiltered max - Where("created_lt > ?", startLt) - - q = r.getFilterMessageQuery(q, &req.MessagesFilter) + With( + "rounded_max_lt", + r.pg.NewSelect(). + Model((*core.Message)(nil)). + ColumnExpr("floor(max(created_lt) / 1e7) * 1e7 AS v"), // we round LT as messages in new blocks can have lower LT + ). + With( + "until_rounded_count", + r.getFilterMessageQuery( + r.pg.NewSelect().Model((*core.Message)(nil)), + &req.MessagesFilter, + ). + Table("rounded_max_lt"). + ColumnExpr("count(*) as v"). + Where("created_lt > ?", startLt). + Where("created_lt <= rounded_max_lt.v"), + ). + With("since_rounded_count", + r.getFilterMessageQuery( + r.pg.NewSelect().Model((*core.Message)(nil)), + &req.MessagesFilter, + ). + Table("rounded_max_lt"). + ColumnExpr("count(*) as v"). + Where("created_lt >= rounded_max_lt.v")). + Table("rounded_max_lt", "until_rounded_count", "since_rounded_count"). + ColumnExpr("since_rounded_count.v AS since_rounded_count"). + ColumnExpr("until_rounded_count.v AS until_rounded_count"). + ColumnExpr("rounded_max_lt.v as rounded_max_lt_value") if err := q.Scan(ctx, &result); err != nil { - return 0, 0, err - } - - if result.MaxLT == 0 { - result.MaxLT = startLt // no new rows + return 0, 0, 0, err } - return result.Count, result.MaxLT, nil + return result.Since + result.Until, result.Until, result.RoundedMaxLT, nil } func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int, error) { @@ -169,11 +207,11 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int return 0, err } - partialCount, maxLT, err := r.countMsgPartialScan(ctx, req, maxLT) + partialCount, roundedPartialCount, roundedMaxLT, err := r.countMsgPartialScan(ctx, req, maxLT) if err != nil { return 0, err } - if err := r.messagesFilterCountCache.Set(req.MessagesFilter, count+partialCount, maxLT); err != nil { + if err := r.messagesFilterCountCache.Set(req.MessagesFilter, count+roundedPartialCount, roundedMaxLT); err != nil { return 0, err } From c68fef72bbdf5d940f4258ab8e4bfe70865d5b5f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sun, 22 Jun 2025 17:52:36 +0300 Subject: [PATCH 087/120] [repo] countMsgPartialScan: fix query --- internal/core/repository/msg/filter.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 91a587e4..2d053a58 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -146,9 +146,9 @@ func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesR func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.MessagesReq, startLt uint64) (partialCount, roundedCount int, roundedMaxLt uint64, err error) { var result struct { - Since int `bun:"since_rounded_count"` - Until int `bun:"until_rounded_count"` - RoundedMaxLT uint64 `bun:"rounded_max_lt_value"` + SinceStartCount int `bun:"since_start_count"` + RoundedCount int `bun:"until_rounded_count"` + RoundedMaxLT uint64 `bun:"rounded_max_lt_value"` } q := r.pg.NewSelect(). @@ -169,16 +169,15 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag Where("created_lt > ?", startLt). Where("created_lt <= rounded_max_lt.v"), ). - With("since_rounded_count", + With("since_start_count", r.getFilterMessageQuery( r.pg.NewSelect().Model((*core.Message)(nil)), &req.MessagesFilter, ). - Table("rounded_max_lt"). ColumnExpr("count(*) as v"). - Where("created_lt >= rounded_max_lt.v")). - Table("rounded_max_lt", "until_rounded_count", "since_rounded_count"). - ColumnExpr("since_rounded_count.v AS since_rounded_count"). + Where("created_lt >= ?", startLt)). + Table("rounded_max_lt", "until_rounded_count", "since_start_count"). + ColumnExpr("since_start_count.v AS since_start_count"). ColumnExpr("until_rounded_count.v AS until_rounded_count"). ColumnExpr("rounded_max_lt.v as rounded_max_lt_value") @@ -186,7 +185,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag return 0, 0, 0, err } - return result.Since + result.Until, result.Until, result.RoundedMaxLT, nil + return result.SinceStartCount, result.RoundedCount, result.RoundedMaxLT, nil } func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int, error) { From cc59ec654976240b0c82502e61849a1d3a4f1828 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 09:43:34 +0300 Subject: [PATCH 088/120] [migrations] fix batch_fill_account_states_created_lt procedure --- .../20250621110511_latest_account_states_created_lt.up.sql | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql index d60ffb8a..4c806214 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -19,14 +19,12 @@ COMMIT; -- iteration_count INT := 0; -- max_address BYTEA; -- BEGIN --- RAISE NOTICE 'Starting batch fill of created_lt with batch size: %', batch_size; --- -- LOOP -- -- Directly query account_states for minimum last_tx_lt per address -- WITH min_tx_lt AS ( -- SELECT address, MIN(last_tx_lt) as min_lt -- FROM account_states --- WHERE (last_processed_address IS NULL OR address > last_processed_address) +-- WHERE address > last_processed_address -- GROUP BY address -- ORDER BY address -- LIMIT batch_size @@ -69,4 +67,5 @@ COMMIT; -- $$; -- -- -- Example usage: --- -- CALL batch_fill_account_states_created_lt(10000); +-- -- CALL batch_fill_account_states_created_lt(60000, decode('000000000000000000000000000000000000000000000000000000000000000000', 'hex')); +-- -- CALL batch_fill_account_states_created_lt(60000, decode('0074b000e63938eb4547be7a5c3011ec6c5cb2fc80f55539b8124c5e4e5851818a', 'hex')); From 8885e849dab02307282b69d0d463779021609784 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 15:18:34 +0300 Subject: [PATCH 089/120] upgrade tonutils-go to v1.13.0 --- .golangci.yaml | 2 +- Dockerfile | 2 +- go.mod | 14 +++++++------- go.sum | 36 ++++++++++++++++++++++-------------- internal/app/fetcher/map.go | 6 +++--- internal/core/tx.go | 4 +++- 6 files changed, 37 insertions(+), 27 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 8f1df8f9..ae58e086 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,5 +1,5 @@ run: - go: '1.19' + go: '1.23' concurrency: 4 timeout: 5m issues-exit-code: 2 diff --git a/Dockerfile b/Dockerfile index 9bb26a4f..f65d0b0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN mkdir /output && cp build/emulator/libemulator.so /output # build -FROM golang:1.21.4-bookworm AS builder +FROM golang:1.23-bookworm AS builder RUN apt-get update && \ apt-get install -y libsecp256k1-1 libsodium23 diff --git a/go.mod b/go.mod index 056e342c..445e7f45 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tonindexer/anton -go 1.19 +go 1.23 replace github.com/uptrace/go-clickhouse v0.3.1 => github.com/iam047801/go-clickhouse v0.0.0-20240229162752-6a94cfc6c817 // go-clickhouse branch with dirty fixes @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.1 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.9.5 + github.com/xssnick/tonutils-go v1.13.0 ) require github.com/gin-contrib/cors v1.4.0 @@ -70,12 +70,12 @@ require ( go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3ed43694..39888974 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaW github.com/allisson/go-env v0.3.0 h1:tUcH3zFXCIT2MLWQp84mV5iifpbG1+poXlqDgRJIYy0= github.com/allisson/go-env v0.3.0/go.mod h1:It6Dwy/LfOpLY/uIJiBpqQFifCosR4vPbnoBt4RYSkM= github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -26,6 +27,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= @@ -48,6 +50,7 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -63,7 +66,8 @@ github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/iam047801/go-clickhouse v0.0.0-20240229162752-6a94cfc6c817 h1:paJ2keiVrkQme/eSn0w7+N3HuPJFASkuXOGGNpuvQJU= @@ -176,6 +180,7 @@ github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 h1:EfWjI6BK/pZAZFDJLKLxAGbz4p3VERZIyA3NrVswiI4= github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02/go.mod h1:EU3WwCvNYFpJjCUI0EKTPVRlYW8kAXy6nUbhOlQl5NE= github.com/uptrace/go-clickhouse/chdebug v0.3.1 h1:eAMrKXmF3MQ2ggdvRb+JZ3wELwLWaE4kTudxNLppgRc= +github.com/uptrace/go-clickhouse/chdebug v0.3.1/go.mod h1:g1TT4y+3ooH/15oJyiE0TiQrWWowtTLEgEtV9P0/PvE= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= @@ -185,8 +190,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/xssnick/tonutils-go v1.9.5 h1:kyjWcSEBQeCyXsIMYhdMWIV5coNQZ/89pCriqBXOayM= -github.com/xssnick/tonutils-go v1.9.5/go.mod h1:p1l1Bxdv9sz6x2jfbuGQUGJn6g5cqg7xsTp8rBHFoJY= +github.com/xssnick/tonutils-go v1.13.0 h1:LV2JzB+CuuWaLQiYNolK+YI3NRQOpS0W+T+N+ctF6VQ= +github.com/xssnick/tonutils-go v1.13.0/go.mod h1:EDe/9D/HZpAenbR+WPMQHICOF0BZWAe01TU5+Vpg08k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= @@ -199,13 +204,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -215,11 +221,13 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -236,8 +244,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -246,15 +254,15 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index cb3b1bbe..9344fa5c 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -273,13 +273,13 @@ func mapTransaction(b *ton.BlockIDExt, raw *tlb.Transaction) (*core.Transaction, } } } - if raw.Description.Description != nil { - c, err := tlb.ToCell(raw.Description.Description) + if raw.Description != nil { + c, err := tlb.ToCell(raw.Description) if err != nil { return nil, errors.Wrap(err, "tx description to cell") } tx.Description = c.ToBOC() - mapTransactionDescription(raw.Description.Description, tx) + mapTransactionDescription(raw.Description, tx) } return tx, nil diff --git a/internal/core/tx.go b/internal/core/tx.go index a4175516..6f382824 100644 --- a/internal/core/tx.go +++ b/internal/core/tx.go @@ -52,7 +52,9 @@ type Transaction struct { } func (tx *Transaction) LoadDescription() error { // TODO: optionally load description in API - var d tlb.TransactionDescription + var d struct { + Description any `tlb:"[TransactionDescriptionOrdinary,TransactionDescriptionStorage,TransactionDescriptionTickTock,TransactionDescriptionSplitPrepare,TransactionDescriptionSplitInstall,TransactionDescriptionMergePrepare,TransactionDescriptionMergeInstall]"` + } c, err := cell.FromBOC(tx.Description) if err != nil { From 509777b059bca427cf05c0e0fe0c583185976bd1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 15:22:50 +0300 Subject: [PATCH 090/120] github actions: update go version for golangci-lint --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 0b3362a5..a5c42b6e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.19' + go-version: '1.23' cache: false - uses: actions/checkout@v3 - name: golangci-lint From 110bc33818914d0cc4e966313a16bb82091e8831 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 15:56:31 +0300 Subject: [PATCH 091/120] golangci-lint: migrate to v2.1 --- .github/workflows/golangci-lint.yml | 11 +- .golangci.yaml | 121 +++++++++++--------- abi/get.go | 2 +- abi/get_emulator.go | 14 +-- abi/tlb.go | 4 +- abi/tlb_types.go | 2 +- addr/address.go | 4 +- addr/address_test.go | 2 +- cmd/contract/interface.go | 2 +- internal/api/http/controller.go | 4 +- internal/app/fetcher/block.go | 4 +- internal/app/fetcher/fetcher_test.go | 4 +- internal/app/fetcher/libraries.go | 2 +- internal/app/fetcher/map.go | 2 +- internal/app/indexer/fetch.go | 6 +- internal/app/parser/account_test.go | 2 +- internal/app/parser/get.go | 16 +-- internal/app/rescan/rescan.go | 4 +- internal/core/aggregate/history/history.go | 10 +- internal/core/repository/account/account.go | 4 +- internal/core/rndm/account.go | 2 +- internal/core/rndm/rndm.go | 4 +- internal/core/rndm/tx.go | 4 +- 23 files changed, 121 insertions(+), 109 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index a5c42b6e..0eaaa492 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,12 +19,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.23' - cache: false - - uses: actions/checkout@v3 + go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - version: v1.52.2 + version: v2.1 diff --git a/.golangci.yaml b/.golangci.yaml index ae58e086..09a6ce8c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,46 +1,36 @@ +version: "2" run: - go: '1.23' concurrency: 4 - timeout: 5m + go: "1.23" + modules-download-mode: readonly issues-exit-code: 2 tests: true - modules-download-mode: readonly allow-parallel-runners: false - skip-files: - - main.go linters: - disable-all: true + default: none enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - - asciicheck - asciicheck - bidichk - decorder - depguard - dupl - durationcheck + - errcheck - errchkjson - errname - errorlint - - execinquery - - exportloopref - forbidigo - forcetypeassert - - goimports - gocognit - goconst - gocritic - gocyclo - - gofmt - goheader - gosec + - govet - grouper - importas + - ineffassign - ireturn - maintidx - makezero @@ -51,42 +41,65 @@ linters: - nolintlint - predeclared - promlinter + - staticcheck - unconvert + - unused - whitespace -linters-settings: - gocyclo: - min-complexity: 18 - gosec: - excludes: - - G404 - gocritic: - disabled-checks: - - regexpMust - - commentedOutCode - - docStub - enabled-tags: - - diagnostic - - style - - performance - - experimental - - opinionated - settings: - captLocal: - paramsOnly: false - elseif: - skipBalanced: false - nestingReduce: - bodyWidth: 4 - rangeValCopy: - sizeThreshold: 64 - skipTestFuncs: false - tooManyResultsChecker: - maxResults: 100 - truncateCmp: - skipArchDependent: false - underef: - skipRecvDeref: false - unnamedResult: - checkExported: true - hugeParam: - sizeThreshold: 64 \ No newline at end of file + settings: + gocritic: + disabled-checks: + - regexpMust + - commentedOutCode + - docStub + enabled-tags: + - diagnostic + - style + - performance + - experimental + - opinionated + settings: + captLocal: + paramsOnly: false + elseif: + skipBalanced: false + hugeParam: + sizeThreshold: 64 + nestingReduce: + bodyWidth: 4 + rangeValCopy: + sizeThreshold: 64 + skipTestFuncs: false + tooManyResultsChecker: + maxResults: 100 + truncateCmp: + skipArchDependent: false + underef: + skipRecvDeref: false + unnamedResult: + checkExported: true + gocyclo: + min-complexity: 18 + gosec: + excludes: + - G404 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/abi/get.go b/abi/get.go index 16b1739e..befb5912 100644 --- a/abi/get.go +++ b/abi/get.go @@ -116,7 +116,7 @@ func GetMethodHashes(code *cell.Cell) ([]int32, error) { case 0, 1, 2, 3: continue } - ret = append(ret, int32(i)) + ret = append(ret, int32(i)) //nolint:gosec // no integer overflow } return ret, nil diff --git a/abi/get_emulator.go b/abi/get_emulator.go index c5bc3dee..7382d2ed 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -106,7 +106,7 @@ func vmMakeValueInt(v *VmValue) (ret tlb.VmStackValue, _ error) { bi, ok = big.NewInt(int64(ui)), uok case "uint64": ui, uok := v.Payload.(uint64) - bi, ok = big.NewInt(int64(ui)), uok + bi, ok = new(big.Int).SetUint64(ui), uok case "int8": ui, uok := v.Payload.(int8) bi, ok = big.NewInt(int64(ui)), uok @@ -285,19 +285,19 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { case "", TLBBigInt: return bi, nil case "uint8": - return uint8(bi.Uint64()), nil + return uint8(bi.Uint64()), nil //nolint:gosec // no integer overflow case "uint16": - return uint16(bi.Uint64()), nil + return uint16(bi.Uint64()), nil //nolint:gosec // no integer overflow case "uint32": - return uint32(bi.Uint64()), nil + return uint32(bi.Uint64()), nil //nolint:gosec // no integer overflow case "uint64": return bi.Uint64(), nil case "int8": - return int8(bi.Int64()), nil + return int8(bi.Int64()), nil //nolint:gosec // no integer overflow case "int16": - return int16(bi.Int64()), nil + return int16(bi.Int64()), nil //nolint:gosec // no integer overflow case "int32": - return int32(bi.Int64()), nil + return int32(bi.Int64()), nil //nolint:gosec // no integer overflow case "int64": return bi.Int64(), nil case TLBBool: diff --git a/abi/tlb.go b/abi/tlb.go index ecd9300b..f1930a42 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -143,12 +143,12 @@ func tlbParseSettingsDict(settings []string) (reflect.Type, error) { func tlbParseSettings(tag string) (reflect.Type, error) { tag = strings.TrimSpace(tag) if tag == "-" { - return nil, nil + return nil, nil //nolint:nilnil // do not want to use a sentinel error here } settings := strings.Split(tag, " ") if len(settings) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // do not want to use a sentinel error here } if strings.HasPrefix(settings[0], "[") && strings.HasSuffix(settings[0], "]") { diff --git a/abi/tlb_types.go b/abi/tlb_types.go index ddf7d73b..0cdd073f 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -50,7 +50,7 @@ func (x *TelemintText) LoadFromCell(loader *cell.Slice) error { return errors.Wrap(err, "load text slice") } - x.Len = uint8(l) + x.Len = uint8(l) //nolint:gosec // no integer overflow x.Text = string(t) return nil diff --git a/addr/address.go b/addr/address.go index e29eb263..7431ac80 100644 --- a/addr/address.go +++ b/addr/address.go @@ -173,7 +173,7 @@ func (x *Address) UnmarshalText(data []byte) error { func (x *Address) Value() (driver.Value, error) { if x == nil { - return nil, nil + return nil, nil //nolint:nilnil // do not want to use a sentinel error here } none := true for _, i := range x { @@ -183,7 +183,7 @@ func (x *Address) Value() (driver.Value, error) { } } if none { - return nil, nil + return nil, nil //nolint:nilnil // do not want to use a sentinel error here } return x[:], nil } diff --git a/addr/address_test.go b/addr/address_test.go index bdf1d6df..c54d776f 100644 --- a/addr/address_test.go +++ b/addr/address_test.go @@ -21,7 +21,7 @@ func TestAddress_TypeKind(t *testing.T) { require.Equal(t, reflect.Uint8, vt.Elem().Elem().Kind()) require.True(t, vt.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem())) - r, err := v.Interface().(driver.Valuer).Value() + r, err := v.Interface().(driver.Valuer).Value() //nolint:forcetypeassert // no need require.Nil(t, err) rb, ok := r.([]byte) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index 0d5c3898..14eba527 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -85,7 +85,7 @@ func ParseOperationDesc(t abi.ContractName, d *abi.OperationDesc) (*core.Contrac if !ok { return nil, fmt.Errorf("wrong hex %s operation id format: %s", d.Name, d.Code) } - opId = uint32(n.Uint64()) + opId = uint32(n.Uint64()) //nolint:gosec // no integer overflow } else { n, err := strconv.ParseUint(c, 10, 32) if err != nil { diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 983a3e9d..e1e401f2 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -83,12 +83,12 @@ func unmarshalOperationID(op string) (uint32, error) { i, err := strconv.ParseInt(op, 10, 64) if err == nil { - return uint32(i), nil + return uint32(i), nil //nolint:gosec // no integer overflow } i, err = strconv.ParseInt(op, 16, 64) if err == nil { - return uint32(i), nil + return uint32(i), nil //nolint:gosec // no integer overflow } return 0, err diff --git a/internal/app/fetcher/block.go b/internal/app/fetcher/block.go index 12cda5ce..98337469 100644 --- a/internal/app/fetcher/block.go +++ b/internal/app/fetcher/block.go @@ -14,7 +14,7 @@ func (s *Service) LookupMaster(ctx context.Context, api ton.APIClientWrapped, se return master, nil } - master, err := api.LookupBlock(ctx, s.masterWorkchain, int64(s.masterShard), seqNo) + master, err := api.LookupBlock(ctx, s.masterWorkchain, int64(s.masterShard), seqNo) //nolint:gosec // no integer overflow if err != nil { return nil, errors.Wrap(err, "lookup masterchain block") } @@ -56,7 +56,7 @@ func (s *Service) getNotSeenShards(ctx context.Context, shard *ton.BlockIDExt, s parents, err := b.BlockInfo.GetParentBlocks() if err != nil { - return nil, fmt.Errorf("get parent blocks (%d:%x:%d): %w", shard.Workchain, uint64(shard.Shard), shard.Shard, err) + return nil, fmt.Errorf("get parent blocks (%d:%x:%d): %w", shard.Workchain, uint64(shard.Shard), shard.Shard, err) //nolint:gosec // no integer overflow } for _, parent := range parents { diff --git a/internal/app/fetcher/fetcher_test.go b/internal/app/fetcher/fetcher_test.go index 886bed14..1fa6dd7d 100644 --- a/internal/app/fetcher/fetcher_test.go +++ b/internal/app/fetcher/fetcher_test.go @@ -58,10 +58,10 @@ func TestService_BlockTransactions(t *testing.T) { ctx := context.Background() - for seq := 29661500; seq < 29661510; seq++ { + for seq := uint32(29661500); seq < 29661510; seq++ { var wg sync.WaitGroup - master, shards, err := s.UnseenBlocks(ctx, uint32(seq)) + master, shards, err := s.UnseenBlocks(ctx, seq) if err != nil { t.Fatal(err) } diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index a7d69d3e..8d31a3be 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -45,7 +45,7 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { } for i := code.RefsNum(); i < 1; i-- { - ref, err := code.PeekRef(int(i - 1)) + ref, err := code.PeekRef(int(i - 1)) //nolint:gosec // no integer overflow if err != nil { return nil, err } diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index 9344fa5c..8882cfaa 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -138,7 +138,7 @@ func parseOperationID(body []byte) (opId uint32, comment string, err error) { return 0, "", errors.Wrap(err, "load uint") } - if opId = uint32(op); opId != 0 { + if opId = uint32(op); opId != 0 { //nolint:gosec // no integer overflow return opId, "", nil } diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index fdf51e31..881e5195 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -16,7 +16,7 @@ import ( func (s *Service) getUnseenBlocks(ctx context.Context, seq uint32) (master *ton.BlockIDExt, shards []*ton.BlockIDExt, err error) { master, shards, err = s.Fetcher.UnseenBlocks(ctx, seq) if err != nil { - if !errors.Is(err, ton.ErrBlockNotFound) && !(err != nil && strings.Contains(err.Error(), "block is not applied")) { + if !errors.Is(err, ton.ErrBlockNotFound) && !strings.Contains(err.Error(), "block is not applied") { return nil, nil, errors.Wrap(err, "cannot fetch unseen blocks") } @@ -125,7 +125,7 @@ func (s *Service) fetchMaster(seq uint32) *core.Block { log.Error(). Err(errBlock.err). Int32("workchain", errBlock.block.Workchain). - Uint64("shard", uint64(errBlock.block.Shard)). + Int64("shard", errBlock.block.Shard). Uint32("seq", errBlock.block.SeqNo). Msg("cannot process block") time.Sleep(time.Second) @@ -189,7 +189,7 @@ func (s *Service) fetchMastersConcurrent(fromBlock uint32, results chan<- *core. for i := 0; i < workers; i++ { go func(seq uint32) { ch <- s.fetchMaster(seq) - }(fromBlock + uint32(i)) + }(fromBlock + uint32(i)) //nolint:gosec // no integer overflow } for i := 0; i < workers; i++ { diff --git a/internal/app/parser/account_test.go b/internal/app/parser/account_test.go index 0728cda5..96a16a83 100644 --- a/internal/app/parser/account_test.go +++ b/internal/app/parser/account_test.go @@ -106,7 +106,7 @@ func TestService_ParseAccountData_NFTItem(t *testing.T) { err = s.ParseAccountData(ctx, ret, others) require.Nil(t, err) require.Equal(t, []abi.ContractName{"nft_item"}, ret.Types) - require.Equal(t, "https://loton.fun/nft/100.json", ret.NFTContentData.ContentURI) + require.Equal(t, "https://loton.fun/nft/100.json", ret.ContentURI) j, err := json.Marshal(ret.ExecutedGetMethods) require.Nil(t, err) require.Equal(t, `{"nft_collection":[{"name":"get_nft_content","address":{"hex":"0:4ccba08d80193c3eb4f92cd8cf10bc425ff2d705a552aad6f3453a141e51b7b7","base64":"EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg"},"receives":["ZA==","te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g=="],"returns":[{"URI":"https://loton.fun/nft/100.json"}]},{"name":"get_nft_address_by_index","address":{"hex":"0:4ccba08d80193c3eb4f92cd8cf10bc425ff2d705a552aad6f3453a141e51b7b7","base64":"EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg"},"receives":["ZA=="],"returns":["EQAQKmY9GTsEb6lREv-vxjT5sVHJyli40xGEYP3tKZSDuTBj"]}],"nft_item":[{"name":"get_nft_data","returns":[true,"ZA==","EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg","EQCIoWk-ZntpYQIRbcaME0ri29yWPEtbL-ay74AJy7KFlcfj","te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g=="]}]}`, string(j)) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 58fbb9b4..b4501dbf 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -146,16 +146,16 @@ func mapContentDataNFT(ret *core.AccountState, c any) { switch content := c.(type) { case *nft.ContentSemichain: // TODO: remove this (?) ret.ContentURI = content.URI - ret.ContentName = content.Name - ret.ContentDescription = content.Description - ret.ContentImage = content.Image - ret.ContentImageData = content.ImageData + ret.ContentName = content.GetAttribute("name") + ret.ContentDescription = content.GetAttribute("description") + ret.ContentImage = content.GetAttribute("image") + ret.ContentImageData = content.GetAttributeBinary("image_data") case *nft.ContentOnchain: - ret.ContentName = content.Name - ret.ContentDescription = content.Description - ret.ContentImage = content.Image - ret.ContentImageData = content.ImageData + ret.ContentName = content.GetAttribute("name") + ret.ContentDescription = content.GetAttribute("description") + ret.ContentImage = content.GetAttribute("image") + ret.ContentImageData = content.GetAttributeBinary("image_data") case *nft.ContentOffchain: ret.ContentURI = content.URI diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index fb320da5..2d31264f 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -84,7 +84,7 @@ func (s *Service) rescanLoop() { for s.running() { tx, task, err := s.RescanRepo.GetUnfinishedRescanTask(context.Background()) if err != nil { - if !(errors.Is(err, core.ErrNotFound) && strings.Contains(err.Error(), "no unfinished tasks")) { + if !errors.Is(err, core.ErrNotFound) || !strings.Contains(err.Error(), "no unfinished tasks") { log.Error().Err(err).Msg("get rescan task") } time.Sleep(time.Second) @@ -108,7 +108,7 @@ func (s *Service) rescanLoop() { } } -func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { //nolint:gocyclo,gocognit // yeah, it's a bit long +func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { //nolint:gocyclo // yeah, it's a bit long var codeHash []byte if task.Contract != nil && task.Contract.Code != nil { codeCell, err := cell.FromBOC(task.Contract.Code) diff --git a/internal/core/aggregate/history/history.go b/internal/core/aggregate/history/history.go index 0156798e..558f8cde 100644 --- a/internal/core/aggregate/history/history.go +++ b/internal/core/aggregate/history/history.go @@ -31,15 +31,15 @@ func GetRoundingFunction(interval time.Duration) (string, error) { sec := int(interval.Seconds()) - min := sec / 60 - if min < 5 { + minutes := sec / 60 + if minutes < 5 { return "", errors.Wrapf(core.ErrInvalidArg, "unsupported interval %d seconds", sec) } - if min < 60 { - return fmt.Sprintf(funcFormat, "%s", min, "minute"), nil + if minutes < 60 { + return fmt.Sprintf(funcFormat, "%s", minutes, "minute"), nil } - hour := min / 60 + hour := minutes / 60 if hour < 24 { return fmt.Sprintf(funcFormat, "%s", hour, "hour"), nil } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index ce452159..10863600 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -467,7 +467,7 @@ func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address if lastInterfaces != nil && reflect.DeepEqual(ret[it].ChangeTypes, *lastInterfaces) { continue } - res[uint64(ret[it].ChangeTxLT)] = ret[it].ChangeTypes + res[uint64(ret[it].ChangeTxLT)] = ret[it].ChangeTypes //nolint:gosec // no integer overflow lastInterfaces = &ret[it].ChangeTypes } @@ -534,7 +534,7 @@ func (r *Repository) GetAllAccountStates(ctx context.Context, a addr.Address, be continue } lastCodeHash, lastDataHash = ret[it].ChangeCodeHash, ret[it].ChangeDataHash - lts = append(lts, uint64(ret[it].ChangeTxLT)) + lts = append(lts, uint64(ret[it].ChangeTxLT)) //nolint:gosec // no integer overflow } if len(lts) > limit { diff --git a/internal/core/rndm/account.go b/internal/core/rndm/account.go index ecd8dcb8..c6ef5587 100644 --- a/internal/core/rndm/account.go +++ b/internal/core/rndm/account.go @@ -18,7 +18,7 @@ var ( func GetMethodHashes() (ret []int32) { for i := 0; i < 1+rand.Int()%16; i++ { - ret = append(ret, int32(rand.Uint32())) + ret = append(ret, int32(rand.Uint32())) //nolint:gosec // no integer overflow } return } diff --git a/internal/core/rndm/rndm.go b/internal/core/rndm/rndm.go index 6a0a5d16..44caea9b 100644 --- a/internal/core/rndm/rndm.go +++ b/internal/core/rndm/rndm.go @@ -11,7 +11,7 @@ import ( ) func init() { - rand.Seed(time.Now().UnixNano()) + rand.Seed(time.Now().UnixNano()) //nolint:staticcheck // TODO: migrate to a local random generator } func String(n int) string { @@ -25,7 +25,7 @@ func String(n int) string { func Bytes(l int) []byte { token := make([]byte, l) - rand.Read(token) + rand.Read(token) //nolint:staticcheck // no need for crypto/rand.Read here return token } diff --git a/internal/core/rndm/tx.go b/internal/core/rndm/tx.go index cdd1b81a..2c9d0b66 100644 --- a/internal/core/rndm/tx.go +++ b/internal/core/rndm/tx.go @@ -28,7 +28,7 @@ func BlockTransaction(b core.BlockID) *core.Transaction { PrevTxLT: rand.Uint64(), InMsgHash: Bytes(32), InAmount: BigInt(), - OutMsgCount: uint16(rand.Int() % 32), + OutMsgCount: uint16(rand.Int() % 32), //nolint:gosec // no integer overflow OutAmount: BigInt(), TotalFees: BigInt(), Description: Bytes(256), @@ -62,7 +62,7 @@ func AddressTransactions(a *addr.Address, n int) (ret []*core.Transaction) { func Transaction() *core.Transaction { return BlockTransaction(core.BlockID{ Workchain: 0, - Shard: int64(rand.Uint64()), + Shard: int64(rand.Uint64()), //nolint:gosec // no integer overflow SeqNo: rand.Uint32(), }) } From ff4a2de3d2b9aed56b28e2d1d452afb168ceb212 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 16:00:01 +0300 Subject: [PATCH 092/120] [repo] message: only 1 hour cache --- internal/core/repository/msg/msg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index f2cf91dc..b74428b7 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -30,7 +30,7 @@ func NewRepository(ck *ch.DB, pg *bun.DB) *Repository { return &Repository{ ch: ck, pg: pg, - messagesFilterCountCache: filter.NewCache(24 * time.Hour), + messagesFilterCountCache: filter.NewCache(time.Hour), } } From d75ca38b562a0a0b4f6907c92959e90f9183f492 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 16:02:31 +0300 Subject: [PATCH 093/120] .golangci.yaml: remove depguard --- .golangci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yaml b/.golangci.yaml index 09a6ce8c..bb21fac7 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -12,7 +12,7 @@ linters: - asciicheck - bidichk - decorder - - depguard + # - depguard - dupl - durationcheck - errcheck From 9633147e44cb076fd2ead8b766ee73a113fa122e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 16:05:10 +0300 Subject: [PATCH 094/120] go.mod: go version 1.23.0 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 445e7f45..2797d72d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tonindexer/anton -go 1.23 +go 1.23.0 replace github.com/uptrace/go-clickhouse v0.3.1 => github.com/iam047801/go-clickhouse v0.0.0-20240229162752-6a94cfc6c817 // go-clickhouse branch with dirty fixes From 88bca3227d8603123577b9a602185e4fd96d8a05 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 16:17:25 +0300 Subject: [PATCH 095/120] [migrations] fix comments --- .../20250620183211_latest_parsed_account_states.up.sql | 3 +-- .../20250621110511_latest_account_states_created_lt.up.sql | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index d27469fd..c90535a5 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -11,7 +11,6 @@ BEGIN; COMMIT; --- --bun:split -- CREATE OR REPLACE PROCEDURE batch_update_latest_parsed_account_states( -- batch_size INT DEFAULT 10000, -- start_from_lt BIGINT DEFAULT 0 @@ -80,4 +79,4 @@ COMMIT; -- $$; -- -- -- Example usage: --- -- CALL batch_update_latest_account_states(100000, 0); +-- -- CALL batch_update_latest_parsed_account_states(100000, 0); diff --git a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql index 4c806214..a447ead5 100644 --- a/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql +++ b/migrations/pgmigrations/20250621110511_latest_account_states_created_lt.up.sql @@ -6,7 +6,6 @@ BEGIN; COMMIT; --- --bun:split -- CREATE OR REPLACE PROCEDURE batch_fill_account_states_created_lt( -- batch_size INT DEFAULT 10000, -- start_from_address BYTEA DEFAULT NULL From 4af29b61fbd6eee15d7cf5478aba7e7d0456be03 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 23 Jun 2025 21:02:57 +0300 Subject: [PATCH 096/120] [migrations] latest_account_states created_lt as not null --- ...0250621110928_latest_account_states_created_lt.down.sql | 0 .../20250621110928_latest_account_states_created_lt.up.sql | 0 ...0250621110928_latest_account_states_created_lt.down.sql | 7 +++++++ .../20250621110928_latest_account_states_created_lt.up.sql | 7 +++++++ 4 files changed, 14 insertions(+) create mode 100644 migrations/chmigrations/20250621110928_latest_account_states_created_lt.down.sql create mode 100644 migrations/chmigrations/20250621110928_latest_account_states_created_lt.up.sql create mode 100644 migrations/pgmigrations/20250621110928_latest_account_states_created_lt.down.sql create mode 100644 migrations/pgmigrations/20250621110928_latest_account_states_created_lt.up.sql diff --git a/migrations/chmigrations/20250621110928_latest_account_states_created_lt.down.sql b/migrations/chmigrations/20250621110928_latest_account_states_created_lt.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/chmigrations/20250621110928_latest_account_states_created_lt.up.sql b/migrations/chmigrations/20250621110928_latest_account_states_created_lt.up.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.down.sql b/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.down.sql new file mode 100644 index 00000000..eb65483e --- /dev/null +++ b/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.down.sql @@ -0,0 +1,7 @@ +SET statement_timeout = 0; + +BEGIN; + DROP INDEX latest_account_states_created_lt_idx; + + ALTER TABLE latest_account_states ALTER COLUMN created_lt DROP NOT NULL; +COMMIT; diff --git a/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.up.sql b/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.up.sql new file mode 100644 index 00000000..bd16a216 --- /dev/null +++ b/migrations/pgmigrations/20250621110928_latest_account_states_created_lt.up.sql @@ -0,0 +1,7 @@ +SET statement_timeout = 0; + +BEGIN; + ALTER TABLE latest_account_states ALTER COLUMN created_lt SET NOT NULL; + + CREATE INDEX latest_account_states_created_lt_idx ON latest_account_states USING btree (created_lt); +COMMIT; From 956cc51688b60eb9b8cd296e2d6f7cb325148639 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 19:52:52 +0300 Subject: [PATCH 097/120] [fetcher] getAccountLibraries: skip nil libraries --- internal/app/fetcher/libraries.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index 8d31a3be..f06bfa96 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" @@ -79,6 +80,11 @@ func (s *Service) getAccountLibraries(ctx context.Context, a addr.Address, raw * for i, hash := range hashes { desc := libDescription{Lib: libs[i]} + if desc.Lib == nil { + log.Error().Str("address", a.Base64()).Hex("hash", hash).Msg("got nil library") + continue + } + t, err := tlb.ToCell(&desc) if err != nil { return nil, err From fa0a123d1b9798b6267a0a5bf17371b10cd3417e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:05:07 +0300 Subject: [PATCH 098/120] [query] FilterAccounts: validate contract types --- internal/app/query/query.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index 1235f77c..00ae0ad1 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -124,6 +124,29 @@ func (s *Service) FilterLabels(ctx context.Context, req *filter.LabelsReq) (*fil return s.accountRepo.FilterLabels(ctx, req) } +func (s *Service) validateContractTypes(ctx context.Context, contractTypes []abi.ContractName) error { + if len(contractTypes) == 0 { + return nil + } + + interfaces, err := s.contractRepo.GetInterfaces(ctx) + if err != nil { + return errors.Wrap(err, "get interfaces") + } + + contractTypesSet := make(map[abi.ContractName]bool) + for _, i := range interfaces { + contractTypesSet[i.Name] = true + } + + for _, t := range contractTypes { + if !contractTypesSet[t] { + return errors.Wrap(core.ErrInvalidArg, "invalid contract type") + } + } + + return nil +} func (s *Service) fetchSkippedAccounts(ctx context.Context, req *filter.AccountsReq, res *filter.AccountsRes) error { if !req.LatestState { return nil // historical states are not available for skipped accounts @@ -215,16 +238,23 @@ func (s *Service) addGetMethodDescription(ctx context.Context, rows []*core.Acco } func (s *Service) FilterAccounts(ctx context.Context, req *filter.AccountsReq) (*filter.AccountsRes, error) { + if err := s.validateContractTypes(ctx, req.ContractTypes); err != nil { + return nil, err + } + res, err := s.accountRepo.FilterAccounts(ctx, req) if err != nil { return nil, err } + if err := s.fetchSkippedAccounts(ctx, req, res); err != nil { return nil, err } + if err := s.addGetMethodDescription(ctx, res.Rows); err != nil { return nil, err } + return res, nil } From ab5bcd9324bd196f36229d6ad4fc090cbe84e5d2 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:06:00 +0300 Subject: [PATCH 099/120] [query] AggregateAccountsHistory: validate contract types --- internal/app/query/query.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index 00ae0ad1..ecfff0fe 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -263,6 +263,9 @@ func (s *Service) AggregateAccounts(ctx context.Context, req *aggregate.Accounts } func (s *Service) AggregateAccountsHistory(ctx context.Context, req *history.AccountsReq) (*history.AccountsRes, error) { + if err := s.validateContractTypes(ctx, req.ContractTypes); err != nil { + return nil, err + } return s.accountRepo.AggregateAccountsHistory(ctx, req) } From f496a0bcebdcf45ce1c7c588ad8b6027925fceb8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:17:14 +0300 Subject: [PATCH 100/120] [query] FilterMessages/AggregateMessagesHistory: validate contract types and operation name --- internal/app/query/query.go | 45 +++++++++++++++++++- internal/core/aggregate/history/msg.go | 5 ++- internal/core/filter/msg.go | 7 +-- internal/core/repository/msg/filter_test.go | 3 +- internal/core/repository/msg/history_test.go | 5 ++- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index ecfff0fe..fdfa9a3b 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -147,6 +147,7 @@ func (s *Service) validateContractTypes(ctx context.Context, contractTypes []abi return nil } + func (s *Service) fetchSkippedAccounts(ctx context.Context, req *filter.AccountsReq, res *filter.AccountsRes) error { if !req.LatestState { return nil // historical states are not available for skipped accounts @@ -277,9 +278,42 @@ func (s *Service) AggregateTransactionsHistory(ctx context.Context, req *history return s.txRepo.AggregateTransactionsHistory(ctx, req) } +func (s *Service) validateOperationNames(ctx context.Context, operationNames []string) error { + if len(operationNames) == 0 { + return nil + } + + operations, err := s.contractRepo.GetOperations(ctx) + if err != nil { + return errors.Wrap(err, "get operations") + } + + operationNamesSet := make(map[string]bool) + for _, op := range operations { + operationNamesSet[op.OperationName] = true + } + + for _, t := range operationNames { + if !operationNamesSet[t] { + return errors.Wrap(core.ErrInvalidArg, "invalid operation name") + } + } + + return nil +} + func (s *Service) FilterMessages(ctx context.Context, req *filter.MessagesReq) (*filter.MessagesRes, error) { + if err := s.validateContractTypes(ctx, req.SrcContracts); err != nil { + return nil, err + } + if err := s.validateContractTypes(ctx, req.DstContracts); err != nil { + return nil, err + } + if err := s.validateOperationNames(ctx, req.OperationNames); err != nil { + return nil, err + } if req.OperationID != nil && len(req.OperationNames) > 0 { - return nil, errors.Wrap(core.ErrInvalidArg, "filter is available either on operation name or operation id") + return nil, errors.Wrap(core.ErrInvalidArg, "filter is available either by operation name or operation id") } return s.msgRepo.FilterMessages(ctx, req) } @@ -289,5 +323,14 @@ func (s *Service) AggregateMessages(ctx context.Context, req *aggregate.Messages } func (s *Service) AggregateMessagesHistory(ctx context.Context, req *history.MessagesReq) (*history.MessagesRes, error) { + if err := s.validateContractTypes(ctx, req.SrcContracts); err != nil { + return nil, err + } + if err := s.validateContractTypes(ctx, req.DstContracts); err != nil { + return nil, err + } + if err := s.validateOperationNames(ctx, req.OperationNames); err != nil { + return nil, err + } return s.msgRepo.AggregateMessagesHistory(ctx, req) } diff --git a/internal/core/aggregate/history/msg.go b/internal/core/aggregate/history/msg.go index 2583055a..edbafd4a 100644 --- a/internal/core/aggregate/history/msg.go +++ b/internal/core/aggregate/history/msg.go @@ -3,6 +3,7 @@ package history import ( "context" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" ) @@ -22,8 +23,8 @@ type MessagesReq struct { SrcWorkchain *int32 `form:"src_workchain"` DstWorkchain *int32 `form:"dst_workchain"` - SrcContracts []string `form:"src_contract"` - DstContracts []string `form:"dst_contract"` + SrcContracts []abi.ContractName `form:"src_contract"` + DstContracts []abi.ContractName `form:"dst_contract"` OperationNames []string `form:"operation_name"` diff --git a/internal/core/filter/msg.go b/internal/core/filter/msg.go index 567d4f23..f4f055eb 100644 --- a/internal/core/filter/msg.go +++ b/internal/core/filter/msg.go @@ -5,6 +5,7 @@ import ( "github.com/uptrace/bun" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" ) @@ -18,9 +19,9 @@ type MessagesFilter struct { SrcWorkchain *int32 `form:"src_workchain"` DstWorkchain *int32 `form:"dst_workchain"` - SrcContracts []string `form:"src_contract"` - DstContracts []string `form:"dst_contract"` - OperationNames []string `form:"operation_name"` + SrcContracts []abi.ContractName `form:"src_contract"` + DstContracts []abi.ContractName `form:"dst_contract"` + OperationNames []string `form:"operation_name"` } type MessagesReq struct { diff --git a/internal/core/repository/msg/filter_test.go b/internal/core/repository/msg/filter_test.go index 0d8ae371..841dc175 100644 --- a/internal/core/repository/msg/filter_test.go +++ b/internal/core/repository/msg/filter_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/filter" @@ -78,7 +79,7 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("filter by contract", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ MessagesFilter: filter.MessagesFilter{ - DstContracts: []string{"special"}, + DstContracts: []abi.ContractName{"special"}, }, Count: true, }) diff --git a/internal/core/repository/msg/history_test.go b/internal/core/repository/msg/history_test.go index 33e05f22..fb6e4c8e 100644 --- a/internal/core/repository/msg/history_test.go +++ b/internal/core/repository/msg/history_test.go @@ -9,6 +9,7 @@ import ( "github.com/uptrace/bun/extra/bunbig" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core/aggregate/history" "github.com/tonindexer/anton/internal/core/rndm" ) @@ -54,7 +55,7 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { t.Run("count messages to special contract", func(t *testing.T) { res, err := repo.AggregateMessagesHistory(ctx, &history.MessagesReq{ Metric: history.MessageCount, - DstContracts: []string{"special"}, + DstContracts: []abi.ContractName{"special"}, ReqParams: history.ReqParams{ From: time.Now().Add(-time.Minute), Interval: 24 * time.Hour, @@ -68,7 +69,7 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { t.Run("sum messages amount to special contract", func(t *testing.T) { res, err := repo.AggregateMessagesHistory(ctx, &history.MessagesReq{ Metric: history.MessageAmountSum, - DstContracts: []string{"special"}, + DstContracts: []abi.ContractName{"special"}, ReqParams: history.ReqParams{ From: time.Now().Add(-time.Minute), Interval: 24 * time.Hour, From b28ec85ee6fe5777122416e2e829ac2cba21ddc0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:34:41 +0300 Subject: [PATCH 101/120] [abi] move emulator to a separate package --- abi/abi.go | 7 + abi/{ => emulator}/get_emulator.go | 111 +++++------- abi/emulator/get_emulator_test.go | 228 +++++++++++++++++++++++++ abi/get.go | 23 +++ abi/get_test.go | 216 ----------------------- abi/known/known_test.go | 3 +- abi/tlb.go | 8 +- abi/tlb_types.go | 12 +- internal/app/fetcher/libraries_test.go | 3 +- internal/app/parser/get.go | 3 +- 10 files changed, 323 insertions(+), 291 deletions(-) rename abi/{ => emulator}/get_emulator.go (84%) create mode 100644 abi/emulator/get_emulator_test.go diff --git a/abi/abi.go b/abi/abi.go index 6f5e28ce..49437d1f 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -24,6 +24,8 @@ type InterfaceDesc struct { ContractData TLBFieldsDesc `json:"contract_data,omitempty"` } +var registeredDefinitions = map[TLBType]TLBFieldsDesc{} + func RegisterDefinitions(definitions map[TLBType]TLBFieldsDesc, depth ...int) error { noDef := map[TLBType]TLBFieldsDesc{} for dn, d := range definitions { @@ -67,3 +69,8 @@ func RegisterDefinitions(definitions map[TLBType]TLBFieldsDesc, depth ...int) er return RegisterDefinitions(noDef, currentDepth+1, maxDepth) } + +func GetRegisteredDefinition(t TLBType) (TLBFieldsDesc, bool) { + desc, ok := registeredDefinitions[t] + return desc, ok +} diff --git a/abi/get_emulator.go b/abi/emulator/get_emulator.go similarity index 84% rename from abi/get_emulator.go rename to abi/emulator/get_emulator.go index 7382d2ed..a4882dc2 100644 --- a/abi/get_emulator.go +++ b/abi/emulator/get_emulator.go @@ -1,4 +1,4 @@ -package abi +package emulator import ( "context" @@ -22,30 +22,9 @@ import ( "github.com/xssnick/tonutils-go/ton/nft" "github.com/xssnick/tonutils-go/tvm/cell" - "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/abi" ) -type VmValue struct { - VmValueDesc - Payload any `json:"payload"` -} - -type VmStack []VmValue - -type GetMethodExecution struct { - Name string `json:"name,omitempty"` - - Address *addr.Address `json:"address,omitempty"` - - Arguments []VmValueDesc `json:"arguments,omitempty"` - Receives []any `json:"receives,omitempty"` - - ReturnValues []VmValueDesc `json:"return_values,omitempty"` - Returns []any `json:"returns,omitempty"` - - Error string `json:"error,omitempty"` -} - var ErrWrongValueFormat = errors.New("wrong value for this format") type Emulator struct { @@ -88,12 +67,12 @@ func NewEmulatorBase64(a *address.Address, code, data, cfg, libraries string) (* return newEmulator(a, e) } -func vmMakeValueInt(v *VmValue) (ret tlb.VmStackValue, _ error) { +func vmMakeValueInt(v *abi.VmValue) (ret tlb.VmStackValue, _ error) { var bi *big.Int var ok bool switch v.Format { - case "", TLBBigInt: + case "", abi.TLBBigInt: bi, ok = v.Payload.(*big.Int) case "uint8": ui, uok := v.Payload.(uint8) @@ -140,14 +119,14 @@ func vmMakeValueInt(v *VmValue) (ret tlb.VmStackValue, _ error) { return ret, nil } -func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { +func vmMakeValueCell(v *abi.VmValue) (tlb.VmStackValue, error) { var c *cell.Cell var ok bool switch v.Format { - case "", TLBCell: + case "", abi.TLBCell: c, ok = v.Payload.(*cell.Cell) - case TLBAddr: + case abi.TLBAddr: a, aok := v.Payload.(*address.Address) if aok { b := cell.BeginCell() @@ -156,7 +135,7 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { } c, ok = b.EndCell(), aok } - case TLBString: + case abi.TLBString: s, sok := v.Payload.(string) if sok { b := cell.BeginCell() @@ -165,7 +144,7 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { } c, ok = b.EndCell(), sok } - case TLBStructCell: + case abi.TLBStructCell: var err error c, err = tutlb.ToCell(v.Payload) if err != nil { @@ -197,14 +176,14 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { return ret, err } -func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { +func vmMakeValueSlice(v *abi.VmValue) (tlb.VmStackValue, error) { var s *cell.Slice var ok bool switch v.Format { - case "", TLBType(VmSlice): + case "", abi.TLBType(abi.VmSlice): s, ok = v.Payload.(*cell.Slice) - case TLBAddr: + case abi.TLBAddr: a, aok := v.Payload.(*address.Address) if aok { b := cell.BeginCell() @@ -213,7 +192,7 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { } s, ok = b.EndCell().BeginParse(), aok } - case TLBString: + case abi.TLBString: a, aok := v.Payload.(string) if aok { b := cell.BeginCell() @@ -222,7 +201,7 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { } s, ok = b.EndCell().BeginParse(), aok } - case TLBStructCell: + case abi.TLBStructCell: c, err := tutlb.ToCell(v.Payload) if err != nil { return tlb.VmStackValue{}, errors.Wrapf(err, "'%s' argument to cell", v.Name) @@ -253,15 +232,15 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { return ret, err } -func vmMakeValue(v *VmValue) (ret tlb.VmStackValue, _ error) { +func vmMakeValue(v *abi.VmValue) (ret tlb.VmStackValue, _ error) { switch v.StackType { - case VmInt: + case abi.VmInt: return vmMakeValueInt(v) - case VmCell: + case abi.VmCell: return vmMakeValueCell(v) - case VmSlice: + case abi.VmSlice: return vmMakeValueSlice(v) default: @@ -269,7 +248,7 @@ func vmMakeValue(v *VmValue) (ret tlb.VmStackValue, _ error) { } } -func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { +func vmParseValueInt(v *tlb.VmStackValue, d *abi.VmValueDesc) (any, error) { var bi *big.Int switch v.SumType { @@ -282,7 +261,7 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } switch d.Format { - case "", TLBBigInt: + case "", abi.TLBBigInt: return bi, nil case "uint8": return uint8(bi.Uint64()), nil //nolint:gosec // no integer overflow @@ -300,45 +279,45 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return int32(bi.Int64()), nil //nolint:gosec // no integer overflow case "int64": return bi.Int64(), nil - case TLBBool: + case abi.TLBBool: return bi.Cmp(big.NewInt(0)) != 0, nil - case TLBBytes: + case abi.TLBBytes: return bi.Bytes(), nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) } } -func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { +func vmParseCell(c *cell.Cell, desc *abi.VmValueDesc) (any, error) { switch desc.Format { - case TLBCell: + case abi.TLBCell: return c, nil - case TLBSlice: + case abi.TLBSlice: return c.BeginParse(), nil - case TLBString: + case abi.TLBString: s, err := c.BeginParse().LoadStringSnake() if err != nil { return nil, errors.Wrap(err, "load string snake") } return s, nil - case TLBAddr: + case abi.TLBAddr: a, err := c.BeginParse().LoadAddr() if err != nil { return nil, errors.Wrap(err, "load address") } return a, nil - case TLBContentCell: + case abi.TLBContentCell: content, err := nft.ContentFromCell(c) if err != nil { return nil, errors.Wrap(err, "load content from cell") } return content, nil - case TLBStructCell: + case abi.TLBStructCell: parsed, err := desc.Fields.FromCell(c) if err != nil { return nil, errors.Wrapf(err, "load struct from cell on %s value description schema", desc.Name) @@ -346,9 +325,9 @@ func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { return parsed, nil default: - d, ok := registeredDefinitions[desc.Format] + d, ok := abi.GetRegisteredDefinition(desc.Format) if !ok { - t, ok := typeNameMap[desc.Format] + t, ok := abi.GetGoTypeTLB(desc.Format) if !ok { return nil, fmt.Errorf("cannot find definition or type for '%s' format", desc.Format) } @@ -369,15 +348,15 @@ func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { } } -func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { +func vmParseValueCell(v *tlb.VmStackValue, desc *abi.VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": switch desc.Format { - case "", TLBCell, TLBStructCell: + case "", abi.TLBCell, abi.TLBStructCell: return (*cell.Cell)(nil), nil - case TLBString: + case abi.TLBString: return "", nil - case TLBContentCell: + case abi.TLBContentCell: return nft.ContentAny(nil), nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) @@ -400,23 +379,23 @@ func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { } if desc.Format == "" && len(desc.Fields) > 0 { - desc.Format = TLBStructCell + desc.Format = abi.TLBStructCell } else if desc.Format == "" { - desc.Format = TLBCell + desc.Format = abi.TLBCell } return vmParseCell(c, desc) } -func vmParseValueSlice(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { +func vmParseValueSlice(v *tlb.VmStackValue, desc *abi.VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": switch desc.Format { case "": return (*cell.Slice)(nil), nil - case TLBAddr: + case abi.TLBAddr: return address.NewAddressNone(), nil - case TLBString: + case abi.TLBString: return "", nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) @@ -439,15 +418,15 @@ func vmParseValueSlice(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { } if desc.Format == "" && len(desc.Fields) > 0 { - desc.Format = TLBStructCell + desc.Format = abi.TLBStructCell } else if desc.Format == "" { - desc.Format = TLBSlice + desc.Format = abi.TLBSlice } return vmParseCell(c, desc) } -func vmParseValue(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { +func vmParseValue(v *tlb.VmStackValue, d *abi.VmValueDesc) (any, error) { switch d.StackType { case "int": return vmParseValueInt(v, d) @@ -463,7 +442,7 @@ func vmParseValue(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } } -func (e *Emulator) RunGetMethod(ctx context.Context, method string, args VmStack, retDesc []VmValueDesc) (ret VmStack, err error) { +func (e *Emulator) RunGetMethod(ctx context.Context, method string, args abi.VmStack, retDesc []abi.VmValueDesc) (ret abi.VmStack, err error) { var params tlb.VmStack for it := range args { @@ -490,7 +469,7 @@ func (e *Emulator) RunGetMethod(ctx context.Context, method string, args VmStack if err != nil { return nil, err } - ret = append(ret, VmValue{VmValueDesc: retDesc[i], Payload: r}) + ret = append(ret, abi.VmValue{VmValueDesc: retDesc[i], Payload: r}) } return ret, nil diff --git a/abi/emulator/get_emulator_test.go b/abi/emulator/get_emulator_test.go new file mode 100644 index 00000000..ad07284a --- /dev/null +++ b/abi/emulator/get_emulator_test.go @@ -0,0 +1,228 @@ +package emulator_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "math/big" + "testing" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton/nft" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/stretchr/testify/require" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/emulator" +) + +var configCell *cell.Cell // mainnet blockchain config + +func init() { + // mainnet blockchain config + config, err := base64.StdEncoding.DecodeString("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") + if err != nil { + panic(err) + } + configCell, err = cell.FromBOC(config) + if err != nil { + panic(err) + } +} + +func TestEmulator_RunGetMethod(t *testing.T) { + // query nft collection get_nft_address_by_index + collection := address.MustParseAddr("EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg") + + collectionCode, err := base64.StdEncoding.DecodeString("te6cckECFAEAAh8AART/APSkE/S88sgLAQIBYgIDAgLNBAUCASAODwTn0QY4BIrfAA6GmBgLjYSK3wfSAYAOmP6Z/2omh9IGmf6mpqGEEINJ6cqClAXUcUG6+CgOhBCFRlgFa4QAhkZYKoAueLEn0BCmW1CeWP5Z+A54tkwCB9gHAbKLnjgvlwyJLgAPGBEuABcYES4AHxgRgZgeACQGBwgJAgEgCgsAYDUC0z9TE7vy4ZJTE7oB+gDUMCgQNFnwBo4SAaRDQ8hQBc8WE8s/zMzMye1Ukl8F4gCmNXAD1DCON4BA9JZvpSCOKQakIIEA+r6T8sGP3oEBkyGgUyW78vQC+gDUMCJUSzDwBiO6kwKkAt4Ekmwh4rPmMDJQREMTyFAFzxYTyz/MzMzJ7VQALDI0AfpAMEFEyFAFzxYTyz/MzMzJ7VQAPI4V1NQwEDRBMMhQBc8WE8s/zMzMye1U4F8EhA/y8AIBIAwNAD1FrwBHAh8AV3gBjIywVYzxZQBPoCE8trEszMyXH7AIAC0AcjLP/gozxbJcCDIywET9AD0AMsAyYAAbPkAdMjLAhLKB8v/ydCACASAQEQAlvILfaiaH0gaZ/qamoYLehqGCxABDuLXTHtRND6QNM/1NTUMBAkXwTQ1DHUMNBxyMsHAc8WzMmAIBIBITAC+12v2omh9IGmf6mpqGDYg6GmH6Yf9IBhAALbT0faiaH0gaZ/qamoYCi+CeAI4APgCwGlAMbg==") + require.Nil(t, err) + collectionData, err := base64.StdEncoding.DecodeString("te6cckECEgEAAmcAA1OAH+KPIWfXRAHhzc8BIGKAZ7CGFDhMB09Wc+npbBemPgcgAAAAAAAAaBABAgMCAAQFART/APSkE/S88sgLBgBLAGQD6IAf4o8hZ9dEAeHNzwEgYoBnsIYUOEwHT1Zz6elsF6Y+BzAARAFodHRwczovL2xvdG9uLmZ1bi9jb2xsZWN0aW9uLmpzb24ALGh0dHBzOi8vbG90b24uZnVuL25mdC8CAWIHCAICzgkKAAmhH5/gBQIBIAsMAgEgEBEC1wyIccAkl8D4NDTAwFxsJJfA+D6QPpAMfoAMXHXIfoAMfoAMPACBLOOFDBsIjRSMscF8uGVAfpA1DAQI/AD4AbTH9M/ghBfzD0UUjC6jocyEDdeMkAT4DA0NDU1ghAvyyaiErrjAl8EhA/y8IA0OABE+kQwcLry4U2AB9lE1xwXy4ZH6QCHwAfpA0gAx+gCCCvrwgBuhIZRTFaCh3iLXCwHDACCSBqGRNuIgwv/y4ZIhjj6CEAUTjZHIUAnPFlALzxZxJEkUVEagcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wAQR5QQKjdb4g8AcnCCEIt3FzUFyMv/UATPFhAkgEBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AACCAo41JvABghDVMnbbEDdEAG1xcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wCTMDI04lUC8AMAOztRNDTP/pAINdJwgCafwH6QNQwECQQI+AwcFltbYAAdAPIyz9YzxYBzxbMye1Ugb+s9wA==") + require.Nil(t, err) + + collectionCodeCell, err := cell.FromBOCMultiRoot(collectionCode) + require.Nil(t, err) + collectionDataCell, err := cell.FromBOCMultiRoot(collectionData) + require.Nil(t, err) + + eCollection, err := emulator.NewEmulator(collection, collectionCodeCell[0], collectionDataCell[0], configCell) + require.Nil(t, err) + + ret, err := eCollection.RunGetMethod(context.Background(), "get_nft_address_by_index", + []abi.VmValue{ + { + VmValueDesc: abi.VmValueDesc{ + Name: "index", + StackType: "int", + }, + Payload: big.NewInt(100), + }, + }, + []abi.VmValueDesc{ + { + Name: "address", + StackType: "slice", + Format: "addr", + }, + }, + ) + require.Nil(t, err) + require.Equal(t, 1, len(ret)) + item, ok := ret[0].Payload.(*address.Address) + require.True(t, ok) + require.Equal(t, "EQAQKmY9GTsEb6lREv-vxjT5sVHJyli40xGEYP3tKZSDuTBj", item.String()) + + // query nft item get_nft_data + itemCode, err := base64.StdEncoding.DecodeString("te6cckECDQEAAdAAART/APSkE/S88sgLAQIBYgIDAgLOBAUACaEfn+AFAgEgBgcCASALDALXDIhxwCSXwPg0NMDAXGwkl8D4PpA+kAx+gAxcdch+gAx+gAw8AIEs44UMGwiNFIyxwXy4ZUB+kDUMBAj8APgBtMf0z+CEF/MPRRSMLqOhzIQN14yQBPgMDQ0NTWCEC/LJqISuuMCXwSED/LwgCAkAET6RDBwuvLhTYAH2UTXHBfLhkfpAIfAB+kDSADH6AIIK+vCAG6EhlFMVoKHeItcLAcMAIJIGoZE24iDC//LhkiGOPoIQBRONkchQCc8WUAvPFnEkSRRURqBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7ABBHlBAqN1viCgBycIIQi3cXNQXIy/9QBM8WECSAQHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAAIICjjUm8AGCENUydtsQN0QAbXFwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AJMwMjTiVQLwAwA7O1E0NM/+kAg10nCAJp/AfpA1DAQJBAj4DBwWW1tgAB0A8jLP1jPFgHPFszJ7VSC/dQQb") + require.Nil(t, err) + itemData, err := base64.StdEncoding.DecodeString("te6cckEBAgEAWAABlQAAAAAAAABkgAmZdBGwAyeH1p8lmxniF4hL/lrgtKpVWt5op0KDyjb28AIihaT5me2lhAhFtxowTSuLb3JY8S1sv5rLvgAnLsoWVgEAEDEwMC5qc29u7rJBww==") + require.Nil(t, err) + + itemCodeCell, err := cell.FromBOCMultiRoot(itemCode) + require.Nil(t, err) + itemDataCell, err := cell.FromBOCMultiRoot(itemData) + require.Nil(t, err) + + eItem, err := emulator.NewEmulator(item, itemCodeCell[0], itemDataCell[0], configCell) + require.Nil(t, err) + + ret, err = eItem.RunGetMethod(context.Background(), "get_nft_data", nil, + []abi.VmValueDesc{ + { + Name: "init", + StackType: "int", + Format: "bool", + }, { + Name: "index", + StackType: "int", + }, { + Name: "collection_address", + StackType: "slice", + Format: "addr", + }, { + Name: "owner_address", + StackType: "slice", + Format: "addr", + }, { + Name: "individual_content", + StackType: "cell", + }, + }, + ) + require.Nil(t, err) + require.Equal(t, 5, len(ret)) + collectionGot, ok := ret[2].Payload.(*address.Address) + require.True(t, ok) + require.Equal(t, collection.String(), collectionGot.String()) + indContent, ok := ret[4].Payload.(*cell.Cell) + require.True(t, ok) + require.NotNil(t, indContent) + require.Equal(t, "te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g==", base64.StdEncoding.EncodeToString(indContent.ToBOC())) + + // query nft collection get_nft_content + ret, err = eCollection.RunGetMethod(context.Background(), "get_nft_content", + []abi.VmValue{ + { + VmValueDesc: abi.VmValueDesc{ + Name: "index", + StackType: "int", + }, + Payload: big.NewInt(100), + }, { + VmValueDesc: abi.VmValueDesc{ + Name: "individual_content", + StackType: "cell", + }, + Payload: indContent, + }, + }, + []abi.VmValueDesc{ + { + Name: "full_content", + StackType: "cell", + Format: "content", + }, + }, + ) + require.Nil(t, err) + require.Equal(t, 1, len(ret)) + contentOffChain, ok := ret[0].Payload.(*nft.ContentOffchain) + require.True(t, ok) + require.Equal(t, "https://loton.fun/nft/100.json", contentOffChain.URI) +} + +func TestEmulator_RunGetMethod_ReturnsDefinition(t *testing.T) { + defJ := []byte(`{ + "native_asset": [ + { + "name": "native_asset", + "tlb_type": "$0000", + "format": "tag" + } + ], + "jetton_asset": [ + { + "name": "jetton_asset", + "tlb_type": "$0001", + "format": "tag" + }, + { + "name": "workchain_id", + "tlb_type": "## 8", + "format": "int8" + }, + { + "name": "jetton_address", + "tlb_type": "## 256" + } + ], + "asset_union": [ + { + "name": "asset", + "tlb_type": ".", + "struct_fields": [ + { + "name": "value", + "tlb_type": "[native_asset,jetton_asset]" + } + ] + } + ] +}`) + + var def map[abi.TLBType]abi.TLBFieldsDesc + + err := json.Unmarshal(defJ, &def) + require.Nil(t, err) + + err = abi.RegisterDefinitions(def) + require.Nil(t, err) + + vault := address.MustParseAddr("EQAf4BMoiqPf0U2ADoNiEatTemiw3UXkt5H90aQpeSKC2l7f") + + vaultCode, err := base64.StdEncoding.DecodeString("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") + require.Nil(t, err) + vaultData, err := base64.StdEncoding.DecodeString("te6cckECBgEAASUAAonAC23M4PIfrYhh8FTrwUryFV/Accw+ZrTHFXhtEHvBQWJ4AWpXt3gjT7xIUxgMmywv35tDAqdyqkXGuq+dbzbZxdUmAAMBAgCHgAvgrJ9r7Ajwe2rgY5w57NFRGpqa2028m2RFaXJBrfsAACIAy1WTa8cB1dJRtnkcRxs3gaw1JkH7hXj0XtkPrf2/JBEBFP8A9KQT9LzyyAsDAgJwBAUA9d4DoOmuQ/SAYEHaidqL2o8cMrcCAUDgsQAhkZYKA54sA/QFANeegZID9gHaz9rL2sji/9ojHHvaiaH0gaYOomWOC+XCBwgeCaY+AwQhNnVH9XQr5egHpn4CYammHgIDqEP2CEOh2j3apiCMIIsEAcpN2oex2oPb4gPl/wAJvyky+DxxHPSj") + require.Nil(t, err) + + vaultCodeCell, err := cell.FromBOCMultiRoot(vaultCode) + require.Nil(t, err) + vaultDataCell, err := cell.FromBOCMultiRoot(vaultData) + require.Nil(t, err) + + eVault, err := emulator.NewEmulator(vault, vaultCodeCell[0], vaultDataCell[0], configCell) + require.Nil(t, err) + + ret, err := eVault.RunGetMethod(context.Background(), "get_asset", nil, []abi.VmValueDesc{ + { + Name: "asset", + StackType: "slice", + Format: "asset_union", + }, + }) + require.Nil(t, err) + + j, err := json.Marshal(ret) + require.Nil(t, err) + require.Equal(t, `[{"name":"asset","stack_type":"slice","format":"asset_union","payload":{"asset":{"value":{"jetton_asset":{},"workchain_id":0,"jetton_address":45985353862647206060987594732861817093328871106941773337270673759241903247880}}}}]`, string(j)) +} diff --git a/abi/get.go b/abi/get.go index befb5912..cecc2a63 100644 --- a/abi/get.go +++ b/abi/get.go @@ -8,6 +8,8 @@ import ( "github.com/sigurn/crc16" "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/tonindexer/anton/addr" ) const getMethodsDictKeySz = 19 @@ -27,12 +29,33 @@ type VmValueDesc struct { Fields TLBFieldsDesc `json:"struct_fields,omitempty"` // Format = "struct" } +type VmValue struct { + VmValueDesc + Payload any `json:"payload"` +} + +type VmStack []VmValue + type GetMethodDesc struct { Name string `json:"name"` Arguments []VmValueDesc `json:"arguments,omitempty"` ReturnValues []VmValueDesc `json:"return_values"` } +type GetMethodExecution struct { + Name string `json:"name,omitempty"` + + Address *addr.Address `json:"address,omitempty"` + + Arguments []VmValueDesc `json:"arguments,omitempty"` + Receives []any `json:"receives,omitempty"` + + ReturnValues []VmValueDesc `json:"return_values,omitempty"` + Returns []any `json:"returns,omitempty"` + + Error string `json:"error,omitempty"` +} + func MethodNameHash(name string) int32 { // https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/crypto/smc-envelope/SmartContract.h#L75 return int32(crc16.Checksum([]byte(name), crc16.MakeTable(crc16.CRC16_XMODEM))) | 0x10000 diff --git a/abi/get_test.go b/abi/get_test.go index 6c3f8157..33a481c0 100644 --- a/abi/get_test.go +++ b/abi/get_test.go @@ -1,35 +1,15 @@ package abi_test import ( - "context" - "encoding/base64" - "encoding/json" - "math/big" "testing" "github.com/stretchr/testify/require" - "github.com/xssnick/tonutils-go/address" - "github.com/xssnick/tonutils-go/ton/nft" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" ) -var configCell *cell.Cell // mainnet blockchain config - -func init() { - // mainnet blockchain config - config, err := base64.StdEncoding.DecodeString("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") - if err != nil { - panic(err) - } - configCell, err = cell.FromBOC(config) - if err != nil { - panic(err) - } -} - func TestHasGetMethod(t *testing.T) { // https://ton.cx/address/EQAiZupbLhdE7UWQgnTirCbIJRg6yxfmkvTDjxsFh33Cu5rM codeBOC := mustBase64(t, "te6cckECDQEAAdAAART/APSkE/S88sgLAQIBYgIDAgLOBAUACaEfn+AFAgEgBgcCASALDALXDIhxwCSXwPg0NMDAXGwkl8D4PpA+kAx+gAxcdch+gAx+gAw8AIEs44UMGwiNFIyxwXy4ZUB+kDUMBAj8APgBtMf0z+CEF/MPRRSMLqOhzIQN14yQBPgMDQ0NTWCEC/LJqISuuMCXwSED/LwgCAkAET6RDBwuvLhTYAH2UTXHBfLhkfpAIfAB+kDSADH6AIIK+vCAG6EhlFMVoKHeItcLAcMAIJIGoZE24iDC//LhkiGOPoIQBRONkchQCc8WUAvPFnEkSRRURqBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7ABBHlBAqN1viCgBycIIQi3cXNQXIy/9QBM8WECSAQHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAAIICjjUm8AGCENUydtsQN0QAbXFwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AJMwMjTiVQLwAwA7O1E0NM/+kAg10nCAJp/AfpA1DAQJBAj4DBwWW1tgAB0A8jLP1jPFgHPFszJ7VSC/dQQb") @@ -53,199 +33,3 @@ func TestGetMethodHashes(t *testing.T) { require.Nil(t, err) require.Equal(t, []int32{0x18fcf}, hashes) } - -func TestEmulator_RunGetMethod(t *testing.T) { - // query nft collection get_nft_address_by_index - collection := address.MustParseAddr("EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg") - - collectionCode, err := base64.StdEncoding.DecodeString("te6cckECFAEAAh8AART/APSkE/S88sgLAQIBYgIDAgLNBAUCASAODwTn0QY4BIrfAA6GmBgLjYSK3wfSAYAOmP6Z/2omh9IGmf6mpqGEEINJ6cqClAXUcUG6+CgOhBCFRlgFa4QAhkZYKoAueLEn0BCmW1CeWP5Z+A54tkwCB9gHAbKLnjgvlwyJLgAPGBEuABcYES4AHxgRgZgeACQGBwgJAgEgCgsAYDUC0z9TE7vy4ZJTE7oB+gDUMCgQNFnwBo4SAaRDQ8hQBc8WE8s/zMzMye1Ukl8F4gCmNXAD1DCON4BA9JZvpSCOKQakIIEA+r6T8sGP3oEBkyGgUyW78vQC+gDUMCJUSzDwBiO6kwKkAt4Ekmwh4rPmMDJQREMTyFAFzxYTyz/MzMzJ7VQALDI0AfpAMEFEyFAFzxYTyz/MzMzJ7VQAPI4V1NQwEDRBMMhQBc8WE8s/zMzMye1U4F8EhA/y8AIBIAwNAD1FrwBHAh8AV3gBjIywVYzxZQBPoCE8trEszMyXH7AIAC0AcjLP/gozxbJcCDIywET9AD0AMsAyYAAbPkAdMjLAhLKB8v/ydCACASAQEQAlvILfaiaH0gaZ/qamoYLehqGCxABDuLXTHtRND6QNM/1NTUMBAkXwTQ1DHUMNBxyMsHAc8WzMmAIBIBITAC+12v2omh9IGmf6mpqGDYg6GmH6Yf9IBhAALbT0faiaH0gaZ/qamoYCi+CeAI4APgCwGlAMbg==") - require.Nil(t, err) - collectionData, err := base64.StdEncoding.DecodeString("te6cckECEgEAAmcAA1OAH+KPIWfXRAHhzc8BIGKAZ7CGFDhMB09Wc+npbBemPgcgAAAAAAAAaBABAgMCAAQFART/APSkE/S88sgLBgBLAGQD6IAf4o8hZ9dEAeHNzwEgYoBnsIYUOEwHT1Zz6elsF6Y+BzAARAFodHRwczovL2xvdG9uLmZ1bi9jb2xsZWN0aW9uLmpzb24ALGh0dHBzOi8vbG90b24uZnVuL25mdC8CAWIHCAICzgkKAAmhH5/gBQIBIAsMAgEgEBEC1wyIccAkl8D4NDTAwFxsJJfA+D6QPpAMfoAMXHXIfoAMfoAMPACBLOOFDBsIjRSMscF8uGVAfpA1DAQI/AD4AbTH9M/ghBfzD0UUjC6jocyEDdeMkAT4DA0NDU1ghAvyyaiErrjAl8EhA/y8IA0OABE+kQwcLry4U2AB9lE1xwXy4ZH6QCHwAfpA0gAx+gCCCvrwgBuhIZRTFaCh3iLXCwHDACCSBqGRNuIgwv/y4ZIhjj6CEAUTjZHIUAnPFlALzxZxJEkUVEagcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wAQR5QQKjdb4g8AcnCCEIt3FzUFyMv/UATPFhAkgEBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AACCAo41JvABghDVMnbbEDdEAG1xcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wCTMDI04lUC8AMAOztRNDTP/pAINdJwgCafwH6QNQwECQQI+AwcFltbYAAdAPIyz9YzxYBzxbMye1Ugb+s9wA==") - require.Nil(t, err) - - collectionCodeCell, err := cell.FromBOCMultiRoot(collectionCode) - require.Nil(t, err) - collectionDataCell, err := cell.FromBOCMultiRoot(collectionData) - require.Nil(t, err) - - eCollection, err := abi.NewEmulator(collection, collectionCodeCell[0], collectionDataCell[0], configCell) - require.Nil(t, err) - - ret, err := eCollection.RunGetMethod(context.Background(), "get_nft_address_by_index", - []abi.VmValue{ - { - VmValueDesc: abi.VmValueDesc{ - Name: "index", - StackType: "int", - }, - Payload: big.NewInt(100), - }, - }, - []abi.VmValueDesc{ - { - Name: "address", - StackType: "slice", - Format: "addr", - }, - }, - ) - require.Nil(t, err) - require.Equal(t, 1, len(ret)) - item, ok := ret[0].Payload.(*address.Address) - require.True(t, ok) - require.Equal(t, "EQAQKmY9GTsEb6lREv-vxjT5sVHJyli40xGEYP3tKZSDuTBj", item.String()) - - // query nft item get_nft_data - itemCode, err := base64.StdEncoding.DecodeString("te6cckECDQEAAdAAART/APSkE/S88sgLAQIBYgIDAgLOBAUACaEfn+AFAgEgBgcCASALDALXDIhxwCSXwPg0NMDAXGwkl8D4PpA+kAx+gAxcdch+gAx+gAw8AIEs44UMGwiNFIyxwXy4ZUB+kDUMBAj8APgBtMf0z+CEF/MPRRSMLqOhzIQN14yQBPgMDQ0NTWCEC/LJqISuuMCXwSED/LwgCAkAET6RDBwuvLhTYAH2UTXHBfLhkfpAIfAB+kDSADH6AIIK+vCAG6EhlFMVoKHeItcLAcMAIJIGoZE24iDC//LhkiGOPoIQBRONkchQCc8WUAvPFnEkSRRURqBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7ABBHlBAqN1viCgBycIIQi3cXNQXIy/9QBM8WECSAQHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAAIICjjUm8AGCENUydtsQN0QAbXFwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AJMwMjTiVQLwAwA7O1E0NM/+kAg10nCAJp/AfpA1DAQJBAj4DBwWW1tgAB0A8jLP1jPFgHPFszJ7VSC/dQQb") - require.Nil(t, err) - itemData, err := base64.StdEncoding.DecodeString("te6cckEBAgEAWAABlQAAAAAAAABkgAmZdBGwAyeH1p8lmxniF4hL/lrgtKpVWt5op0KDyjb28AIihaT5me2lhAhFtxowTSuLb3JY8S1sv5rLvgAnLsoWVgEAEDEwMC5qc29u7rJBww==") - require.Nil(t, err) - - itemCodeCell, err := cell.FromBOCMultiRoot(itemCode) - require.Nil(t, err) - itemDataCell, err := cell.FromBOCMultiRoot(itemData) - require.Nil(t, err) - - eItem, err := abi.NewEmulator(item, itemCodeCell[0], itemDataCell[0], configCell) - require.Nil(t, err) - - ret, err = eItem.RunGetMethod(context.Background(), "get_nft_data", nil, - []abi.VmValueDesc{ - { - Name: "init", - StackType: "int", - Format: "bool", - }, { - Name: "index", - StackType: "int", - }, { - Name: "collection_address", - StackType: "slice", - Format: "addr", - }, { - Name: "owner_address", - StackType: "slice", - Format: "addr", - }, { - Name: "individual_content", - StackType: "cell", - }, - }, - ) - require.Nil(t, err) - require.Equal(t, 5, len(ret)) - collectionGot, ok := ret[2].Payload.(*address.Address) - require.True(t, ok) - require.Equal(t, collection.String(), collectionGot.String()) - indContent, ok := ret[4].Payload.(*cell.Cell) - require.True(t, ok) - require.NotNil(t, indContent) - require.Equal(t, "te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g==", base64.StdEncoding.EncodeToString(indContent.ToBOC())) - - // query nft collection get_nft_content - ret, err = eCollection.RunGetMethod(context.Background(), "get_nft_content", - []abi.VmValue{ - { - VmValueDesc: abi.VmValueDesc{ - Name: "index", - StackType: "int", - }, - Payload: big.NewInt(100), - }, { - VmValueDesc: abi.VmValueDesc{ - Name: "individual_content", - StackType: "cell", - }, - Payload: indContent, - }, - }, - []abi.VmValueDesc{ - { - Name: "full_content", - StackType: "cell", - Format: "content", - }, - }, - ) - require.Nil(t, err) - require.Equal(t, 1, len(ret)) - contentOffChain, ok := ret[0].Payload.(*nft.ContentOffchain) - require.True(t, ok) - require.Equal(t, "https://loton.fun/nft/100.json", contentOffChain.URI) -} - -func TestEmulator_RunGetMethod_ReturnsDefinition(t *testing.T) { - defJ := []byte(`{ - "native_asset": [ - { - "name": "native_asset", - "tlb_type": "$0000", - "format": "tag" - } - ], - "jetton_asset": [ - { - "name": "jetton_asset", - "tlb_type": "$0001", - "format": "tag" - }, - { - "name": "workchain_id", - "tlb_type": "## 8", - "format": "int8" - }, - { - "name": "jetton_address", - "tlb_type": "## 256" - } - ], - "asset_union": [ - { - "name": "asset", - "tlb_type": ".", - "struct_fields": [ - { - "name": "value", - "tlb_type": "[native_asset,jetton_asset]" - } - ] - } - ] -}`) - - var def map[abi.TLBType]abi.TLBFieldsDesc - - err := json.Unmarshal(defJ, &def) - require.Nil(t, err) - - err = abi.RegisterDefinitions(def) - require.Nil(t, err) - - vault := address.MustParseAddr("EQAf4BMoiqPf0U2ADoNiEatTemiw3UXkt5H90aQpeSKC2l7f") - - vaultCode, err := base64.StdEncoding.DecodeString("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") - require.Nil(t, err) - vaultData, err := base64.StdEncoding.DecodeString("te6cckECBgEAASUAAonAC23M4PIfrYhh8FTrwUryFV/Accw+ZrTHFXhtEHvBQWJ4AWpXt3gjT7xIUxgMmywv35tDAqdyqkXGuq+dbzbZxdUmAAMBAgCHgAvgrJ9r7Ajwe2rgY5w57NFRGpqa2028m2RFaXJBrfsAACIAy1WTa8cB1dJRtnkcRxs3gaw1JkH7hXj0XtkPrf2/JBEBFP8A9KQT9LzyyAsDAgJwBAUA9d4DoOmuQ/SAYEHaidqL2o8cMrcCAUDgsQAhkZYKA54sA/QFANeegZID9gHaz9rL2sji/9ojHHvaiaH0gaYOomWOC+XCBwgeCaY+AwQhNnVH9XQr5egHpn4CYammHgIDqEP2CEOh2j3apiCMIIsEAcpN2oex2oPb4gPl/wAJvyky+DxxHPSj") - require.Nil(t, err) - - vaultCodeCell, err := cell.FromBOCMultiRoot(vaultCode) - require.Nil(t, err) - vaultDataCell, err := cell.FromBOCMultiRoot(vaultData) - require.Nil(t, err) - - eVault, err := abi.NewEmulator(vault, vaultCodeCell[0], vaultDataCell[0], configCell) - require.Nil(t, err) - - ret, err := eVault.RunGetMethod(context.Background(), "get_asset", nil, []abi.VmValueDesc{ - { - Name: "asset", - StackType: "slice", - Format: "asset_union", - }, - }) - require.Nil(t, err) - - j, err := json.Marshal(ret) - require.Nil(t, err) - require.Equal(t, `[{"name":"asset","stack_type":"slice","format":"asset_union","payload":{"asset":{"value":{"jetton_asset":{},"workchain_id":0,"jetton_address":45985353862647206060987594732861817093328871106941773337270673759241903247880}}}}]`, string(j)) -} diff --git a/abi/known/known_test.go b/abi/known/known_test.go index 83885cb3..eb14a6f9 100644 --- a/abi/known/known_test.go +++ b/abi/known/known_test.go @@ -12,6 +12,7 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/emulator" ) var configCell *cell.Cell @@ -98,7 +99,7 @@ func execGetMethod(t *testing.T, i *abi.InterfaceDesc, addr *address.Address, me dataCell, err := cell.FromBOCMultiRoot(data) require.Nil(t, err) - e, err := abi.NewEmulator(addr, codeCell[0], dataCell[0], configCell) + e, err := emulator.NewEmulator(addr, codeCell[0], dataCell[0], configCell) require.Nil(t, err) stack, err := e.RunGetMethod(context.Background(), methodName, nil, dp.ReturnValues) diff --git a/abi/tlb.go b/abi/tlb.go index f1930a42..239ac560 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -45,7 +45,7 @@ func tlbMakeDesc(t reflect.Type, skipMagic ...bool) (ret TLBFieldsDesc, err erro continue // skip tlb constructor tag as it has to be inside OperationDesc } - ft, ok := typeNameRMap[f.Type] + ft, ok := GetGoTypeNameTLB(f.Type) switch { case ok: schema.Format = ft @@ -155,7 +155,7 @@ func tlbParseSettings(tag string) (reflect.Type, error) { for _, dn := range strings.Split(tag[1:len(tag)-1], ",") { // iterate through union definitions // check that all definitions are known - _, ok := registeredDefinitions[TLBType(dn)] + _, ok := GetRegisteredDefinition(TLBType(dn)) if !ok { return nil, fmt.Errorf("cannot find definition for '%s' type inside union", dn) } @@ -201,7 +201,7 @@ func tlbParseSettings(tag string) (reflect.Type, error) { } func tlbMapFormat(format TLBType, tag string) (reflect.Type, error) { - t, ok := typeNameMap[format] + t, ok := GetGoTypeTLB(format) if ok { return t, nil } @@ -216,7 +216,7 @@ func tlbMapFormat(format TLBType, tag string) (reflect.Type, error) { return t, nil default: - d, ok := registeredDefinitions[format] + d, ok := GetRegisteredDefinition(format) if !ok { return nil, fmt.Errorf("cannot find definition for '%s' format", format) } diff --git a/abi/tlb_types.go b/abi/tlb_types.go index 0cdd073f..7fa86bb8 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -166,8 +166,6 @@ var ( "telemintText": reflect.TypeOf((*TelemintText)(nil)), "dedustAsset": reflect.TypeOf((*DedustAsset)(nil)), } - - registeredDefinitions = map[TLBType]TLBFieldsDesc{} ) func init() { @@ -175,3 +173,13 @@ func init() { typeNameRMap[t] = n } } + +func GetGoTypeTLB(t TLBType) (reflect.Type, bool) { + ret, ok := typeNameMap[t] + return ret, ok +} + +func GetGoTypeNameTLB(t reflect.Type) (TLBType, bool) { + ret, ok := typeNameRMap[t] + return ret, ok +} diff --git a/internal/app/fetcher/libraries_test.go b/internal/app/fetcher/libraries_test.go index f2e6d22a..87f99732 100644 --- a/internal/app/fetcher/libraries_test.go +++ b/internal/app/fetcher/libraries_test.go @@ -9,6 +9,7 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/emulator" "github.com/tonindexer/anton/addr" ) @@ -67,7 +68,7 @@ func TestService_getAccountLibraries_emulate(t *testing.T) { base64.StdEncoding.EncodeToString(acc.Data), base64.StdEncoding.EncodeToString(acc.Libraries) - e, err := abi.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, bcConfigBase64, librariesBase64) + e, err := emulator.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, bcConfigBase64, librariesBase64) require.NoError(t, err) retValues := []abi.VmValueDesc{ diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index b4501dbf..df360c2f 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -17,6 +17,7 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/emulator" "github.com/tonindexer/anton/abi/known" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" @@ -66,7 +67,7 @@ func (s *Service) emulateGetMethod(ctx context.Context, d *abi.GetMethodDesc, ac base64.StdEncoding.EncodeToString(acc.Data), base64.StdEncoding.EncodeToString(acc.Libraries) - e, err := abi.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, s.bcConfigBase64, librariesBase64) + e, err := emulator.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, s.bcConfigBase64, librariesBase64) if err != nil { return ret, errors.Wrap(err, "new emulator") } From ec148bc4fe00a65a0f95936657a6f95d601933eb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:48:26 +0300 Subject: [PATCH 102/120] README.md: provide emulator library path to run tests --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5aa56eba..aba39a71 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,12 @@ Run tests on abi package: go test -p 1 $(go list ./... | grep /abi) -covermode=count ``` +To run the tests you might need to provide a path to the emulator library. For example: + +```shell +CGO_LDFLAGS="-L /Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -Wl,-rpath,/Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -l emulator" go test -p 1 $(go list ./... | grep /abi) -covermode=count +``` + Run repositories tests: ```shell From 4d6db49ca37e571428a22cd1dd5c121517fbda22 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 24 Jun 2025 20:53:20 +0300 Subject: [PATCH 103/120] [filter] cache: cleanup old entries on Get --- internal/core/filter/cache.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/core/filter/cache.go b/internal/core/filter/cache.go index 1f51381d..079d0ef4 100644 --- a/internal/core/filter/cache.go +++ b/internal/core/filter/cache.go @@ -18,12 +18,14 @@ type Cache struct { msgCountCache map[string]CacheEntry msgCountCacheMx sync.Mutex msgCountCacheTTL time.Duration + lastCleanup time.Time } func NewCache(ttl time.Duration) *Cache { return &Cache{ msgCountCache: make(map[string]CacheEntry), msgCountCacheTTL: ttl, + lastCleanup: time.Now(), } } @@ -53,6 +55,19 @@ func (c *Cache) Set(filterReq any, count int, maxSeqNo uint64) error { return nil } +func (c *Cache) cleanupExpiredEntries() { + if time.Since(c.lastCleanup) < time.Minute { + return + } + now := time.Now() + for k, entry := range c.msgCountCache { + if now.Sub(entry.UpdatedAt) > c.msgCountCacheTTL { + delete(c.msgCountCache, k) + } + } + c.lastCleanup = now +} + func (c *Cache) Get(filterReq any) (count int, maxSeqNo uint64, err error) { k, err := getCacheKey(filterReq) if err != nil { @@ -62,14 +77,12 @@ func (c *Cache) Get(filterReq any) (count int, maxSeqNo uint64, err error) { c.msgCountCacheMx.Lock() defer c.msgCountCacheMx.Unlock() + c.cleanupExpiredEntries() + entry, ok := c.msgCountCache[k] if !ok { return 0, 0, core.ErrNotFound } - if time.Since(entry.UpdatedAt) > c.msgCountCacheTTL { - delete(c.msgCountCache, k) - return 0, 0, core.ErrNotFound - } return entry.Count, entry.MaxSeqNo, nil } From 203c890526d28de422d58cb9fec0b54abd36c5f8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 16:52:40 +0300 Subject: [PATCH 104/120] [statistics] getTransactionStatistics: do not panic if there are no messages --- internal/core/aggregate/aggregate.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/core/aggregate/aggregate.go b/internal/core/aggregate/aggregate.go index e7d7da61..970b95c9 100644 --- a/internal/core/aggregate/aggregate.go +++ b/internal/core/aggregate/aggregate.go @@ -177,19 +177,21 @@ func getTransactionStatistics(ctx context.Context, ck *ch.DB, ret *Statistics) e return errors.Wrap(err, "message types count") } - unknownOp := -1 - for it, row := range ret.MessageTypesCount { - ret.MessageCount += row.Count - if row.Operation != "" { - ret.ParsedMessageCount += row.Count - } else { - unknownOp = it + if len(ret.MessageTypesCount) > 0 { + unknownOp := -1 + for it, row := range ret.MessageTypesCount { + ret.MessageCount += row.Count + if row.Operation != "" { + ret.ParsedMessageCount += row.Count + } else { + unknownOp = it + } + } + if unknownOp == len(ret.MessageTypesCount)-1 { + ret.MessageTypesCount = ret.MessageTypesCount[:unknownOp] + } else if unknownOp != -1 { + ret.MessageTypesCount = append(ret.MessageTypesCount[:unknownOp], ret.MessageTypesCount[unknownOp+1:]...) } - } - if unknownOp == len(ret.MessageTypesCount)-1 { - ret.MessageTypesCount = ret.MessageTypesCount[:unknownOp] - } else if unknownOp != -1 { - ret.MessageTypesCount = append(ret.MessageTypesCount[:unknownOp], ret.MessageTypesCount[unknownOp+1:]...) } ret.TransactionCount, err = ck.NewSelect().Model((*core.Transaction)(nil)).Count(ctx) From f34d06e9ae58cce364b1d1c53d979bfbc61f8f07 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 18:50:28 +0300 Subject: [PATCH 105/120] [indexer] getMessagesSource: fail on unknown message source after 16 blocks --- internal/app/indexer/save.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index 8f8a9f1c..2cb370f3 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -171,7 +171,7 @@ func (s *Service) getMessagesSource(ctx context.Context, messages []*core.Messag panic(errors.Wrap(err, "count masterchain blocks")) } } - if totalBlocks < 1000 { + if totalBlocks < 16 { log.Debug(). Hex("dst_tx_hash", msg.DstTxHash). Int32("dst_workchain", msg.DstWorkchain).Int64("dst_shard", msg.DstShard).Uint32("dst_block_seq_no", msg.DstBlockSeqNo). @@ -180,8 +180,8 @@ func (s *Service) getMessagesSource(ctx context.Context, messages []*core.Messag continue } - panic(fmt.Errorf("unknown source of message with dst tx hash %x on block (%d, %d, %d) from %s to %s", - msg.DstTxHash, msg.DstWorkchain, msg.DstShard, msg.DstBlockSeqNo, msg.SrcAddress.String(), msg.DstAddress.String())) + panic(fmt.Errorf("unknown source of message with hash %x and dst tx hash %x on block (%d, %d, %d) from %s to %s", + msg.Hash, msg.DstTxHash, msg.DstWorkchain, msg.DstShard, msg.DstBlockSeqNo, msg.SrcAddress.String(), msg.DstAddress.String())) } return valid From 7970fa5f66021e2ed4ffeba268d8c96b6459ae2a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 18:52:17 +0300 Subject: [PATCH 106/120] [fetcher] mapMessage: calculate message hash as in old versions of tonutils-go --- internal/app/fetcher/map.go | 6 +- internal/app/fetcher/msg_hash/store.go | 424 ++++++++++++++++++++ internal/app/fetcher/msg_hash/store_test.go | 32 ++ 3 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 internal/app/fetcher/msg_hash/store.go create mode 100644 internal/app/fetcher/msg_hash/store_test.go diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index 8882cfaa..e8a722b2 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -11,6 +11,7 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/app/fetcher/msg_hash" "github.com/tonindexer/anton/internal/core" ) @@ -156,11 +157,10 @@ func mapMessage(tx *tlb.Transaction, message tlb.Message) (*core.Message, error) err error ) - msgCell, err := tlb.ToCell(message.Msg) + msg.Hash, err = msg_hash.GetMessageHash(message.Msg) if err != nil { - return nil, errors.Wrap(err, "cannot convert message to cell") + return nil, err } - msg.Hash = msgCell.Hash() switch raw := message.Msg.(type) { case *tlb.InternalMessage: diff --git a/internal/app/fetcher/msg_hash/store.go b/internal/app/fetcher/msg_hash/store.go new file mode 100644 index 00000000..2c960284 --- /dev/null +++ b/internal/app/fetcher/msg_hash/store.go @@ -0,0 +1,424 @@ +package msg_hash + +import ( + "fmt" + "math/big" + "reflect" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +func GetMessageHash(msg tlb.AnyMessage) ([]byte, error) { + msgCell, err := toCell(msg) + if err != nil { + return nil, errors.Wrap(err, "cannot convert message to cell") + } + return msgCell.Hash(), nil +} + +// copied from tlb.ToCell of tonutils-go@v1.13.0 +// the only patch is to disable "store cell as ref directly" in storeField + +func toCell(v any) (*cell.Cell, error) { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer { + if rv.IsNil() { + return nil, fmt.Errorf("v should not be nil") + } + rv = rv.Elem() + } + + if ld, ok := v.(tlb.Marshaller); ok { + c, err := ld.ToCell() + if err != nil { + return nil, fmt.Errorf("failed to store to cell for %s, using manual storer, err: %w", reflect.TypeOf(v).PkgPath(), err) + } + return c, nil + } + + root := cell.BeginCell() + +next: + for i := 0; i < rv.NumField(); i++ { + structField := rv.Type().Field(i) + parseType := structField.Type + fieldVal := rv.Field(i) + tag := strings.TrimSpace(structField.Tag.Get("tlb")) + if tag == "-" { + continue + } + settings := strings.Split(tag, " ") + + if len(settings) == 0 { + continue + } + + if settings[0][0] == '?' { + // conditional tlb parse depending on some field value of this struct + cond := rv.FieldByName(settings[0][1:]) + if !cond.Bool() { + continue + } + settings = settings[1:] + } + + if settings[0] == "maybe" { + if structField.Type.Kind() != reflect.Pointer && structField.Type.Kind() != reflect.Interface && structField.Type.Kind() != reflect.Slice { + return nil, fmt.Errorf("maybe flag can only be applied to interface or pointer, field %s", structField.Name) + } + + if fieldVal.IsNil() { + if err := root.StoreBoolBit(false); err != nil { + return nil, fmt.Errorf("cannot store maybe bit: %w", err) + } + continue + } + + if err := root.StoreBoolBit(true); err != nil { + return nil, fmt.Errorf("cannot store maybe bit: %w", err) + } + settings = settings[1:] + } + + if structField.Type.Kind() == reflect.Pointer && structField.Type.Elem().Kind() != reflect.Struct { + // to same process both pointers and types + parseType = parseType.Elem() + fieldVal = fieldVal.Elem() + } + + if settings[0] == "either" { + settings = settings[1:] + + if len(settings) < 2 { + panic("either tag should have 2 args") + } + + leaveBits, leaveRefs := 0, 0 + if settings[0] == "leave" { + settings = settings[1:] + + if len(settings) < 3 { + panic("either tag should have 2 args and leave tag should have 1 arg") + } + + spl := strings.Split(settings[0], ",") + settings = settings[1:] + + val, err := strconv.ParseUint(spl[0], 10, 10) + if err != nil { + panic("invalid argument for either leave bits") + } + // set how many free bits we need to have after either written + leaveBits = int(val) + + if len(spl) > 1 { + val, err = strconv.ParseUint(spl[1], 10, 10) + if err != nil { + panic("invalid argument for either leave refs") + } + // set how many free efs we need to have after either written + leaveRefs = int(val) + } + } + + // we try first option, if it is overflows then we try second + for x := 0; x < 2; x++ { + builder := cell.BeginCell() + if err := storeField([]string{settings[x]}, builder, structField, fieldVal, parseType); err != nil { + return nil, fmt.Errorf("failed to serialize field %s to cell as either %d: %w", structField.Name, x, err) + } + + // check if we have enough free bits + if x == 0 && (int(root.BitsLeft())-int(builder.BitsUsed()+1) < leaveBits || int(root.RefsLeft())-int(builder.RefsUsed()) < leaveRefs) { + // if not, then we try second option + continue + } + + if err := root.StoreUInt(uint64(x), 1); err != nil { + return nil, fmt.Errorf("cannot store either bit: %w", err) + } + if err := root.StoreBuilder(builder); err != nil { + return nil, fmt.Errorf("failed to concat builder of field %s to cell as either %d: %w", structField.Name, x, err) + } + + continue next + } + + return nil, fmt.Errorf("failed to serialize either field %s to cell: no valid options", structField.Name) + } + + if err := storeField(settings, root, structField, fieldVal, parseType); err != nil { + return nil, fmt.Errorf("failed to serialize field %s to cell: %w", structField.Name, err) + } + } + + return root.EndCell(), nil +} + +var cellType = reflect.TypeOf(&cell.Cell{}) + +func storeField(settings []string, root *cell.Builder, structField reflect.StructField, fieldVal reflect.Value, parseType reflect.Type) error { + builder := root + + asRef := false + if settings[0] == "^" { + // if cellType == parseType { // we disable this + // // store cell as ref directly + // if err := root.StoreRef(fieldVal.Interface().(*cell.Cell)); err != nil { + // return fmt.Errorf("failed to store cell to ref for %s, err: %w", structField.Name, err) + // } + // return nil + // } + + asRef = true + settings = settings[1:] + builder = cell.BeginCell() + } + + if structField.Type.Kind() == reflect.Interface { + allowed := strings.Join(settings, "") + if !strings.HasPrefix(allowed, "[") || !strings.HasSuffix(allowed, "]") { + panic("corrupted allowed list tag of field " + structField.Name + ", should be [a,b,c], got " + allowed) + } + + // cut brackets + allowed = allowed[1 : len(allowed)-1] + types := strings.Split(allowed, ",") + + t := fieldVal.Elem().Type() + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + + found := false + for _, typ := range types { + if t.Name() == typ { + found = true + break + } + } + + if !found { + return fmt.Errorf("unexpected data to serialize, not registered magic in tag for %s", t.String()) + } + settings = settings[:0] + } + + if len(settings) == 0 || settings[0] == "." { + c, err := structStore(fieldVal, structField.Type.Name()) + if err != nil { + return err + } + + err = builder.StoreBuilder(c.ToBuilder()) + if err != nil { + return fmt.Errorf("failed to store cell to builder for %s, err: %w", structField.Name, err) + } + } else if settings[0] == "##" { + num, err := strconv.ParseUint(settings[1], 10, 64) + if err != nil { + // we panic, because its developer's issue, need to fix tag + panic("corrupted num bits in ## tag") + } + + switch { + case num <= 64: + switch parseType.Kind() { + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + err = builder.StoreInt(fieldVal.Int(), uint(num)) + if err != nil { + return fmt.Errorf("failed to store int %d, err: %w", num, err) + } + case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: + err = builder.StoreUInt(fieldVal.Uint(), uint(num)) + if err != nil { + return fmt.Errorf("failed to store int %d, err: %w", num, err) + } + default: + if parseType == reflect.TypeOf(&big.Int{}) { + err = builder.StoreBigInt(fieldVal.Interface().(*big.Int), uint(num)) + if err != nil { + return fmt.Errorf("failed to store bigint %d, err: %w", num, err) + } + } else { + panic("unexpected field type for tag ## - " + parseType.String()) + } + } + case num <= 256: + err := builder.StoreBigInt(fieldVal.Interface().(*big.Int), uint(num)) + if err != nil { + return fmt.Errorf("failed to store bigint %d, err: %w", num, err) + } + } + } else if settings[0] == "addr" { + err := builder.StoreAddr(fieldVal.Interface().(*address.Address)) + if err != nil { + return fmt.Errorf("failed to store address, err: %w", err) + } + } else if settings[0] == "bool" { + err := builder.StoreBoolBit(fieldVal.Bool()) + if err != nil { + return fmt.Errorf("failed to store bool, err: %w", err) + } + } else if settings[0] == "bits" { + num, err := strconv.Atoi(settings[1]) + if err != nil { + // we panic, because its developer's issue, need to fix tag + panic("corrupted num bits in bits tag") + } + + err = builder.StoreSlice(fieldVal.Bytes(), uint(num)) + if err != nil { + return fmt.Errorf("failed to store bits %d, err: %w", num, err) + } + } else if parseType == reflect.TypeOf(tlb.Magic{}) { + var sz, base int + if strings.HasPrefix(settings[0], "#") { + base = 16 + sz = (len(settings[0]) - 1) * 4 + } else if strings.HasPrefix(settings[0], "$") { + base = 2 + sz = len(settings[0]) - 1 + } else { + panic("unknown magic value type in tag") + } + + if sz > 64 { + panic("too big magic value type in tag") + } + + magic, err := strconv.ParseInt(settings[0][1:], base, 64) + if err != nil { + panic("corrupted magic value in tag") + } + + err = builder.StoreUInt(uint64(magic), uint(sz)) + if err != nil { + return fmt.Errorf("failed to store magic: %w", err) + } + } else if settings[0] == "dict" { + var dict *cell.Dictionary + + settings = settings[1:] + + isInline := len(settings) > 0 && settings[0] == "inline" + if isInline { + settings = settings[1:] + } + + if len(settings) < 3 || settings[1] != "->" { + dict = fieldVal.Interface().(*cell.Dictionary) + } else { + if fieldVal.Kind() != reflect.Map { + return fmt.Errorf("want to create dictionary from map, but instead got %s type", fieldVal.Type()) + } + if fieldVal.Type().Key() != reflect.TypeOf("") { + return fmt.Errorf("map key should be string, but instead got %s type", fieldVal.Type().Key()) + } + + sz, err := strconv.ParseUint(settings[0], 10, 64) + if err != nil { + panic(fmt.Sprintf("cannot deserialize field '%s' as dict, bad size '%s'", structField.Name, settings[0])) + } + + dict = cell.NewDict(uint(sz)) + + for _, mapK := range fieldVal.MapKeys() { + mapKI, ok := big.NewInt(0).SetString(mapK.Interface().(string), 10) + if !ok { + return fmt.Errorf("cannot parse '%s' map key to big int of '%s' field", mapK.Interface().(string), structField.Name) + } + + mapKB := cell.BeginCell() + if err := mapKB.StoreBigInt(mapKI, uint(sz)); err != nil { + return fmt.Errorf("store big int of size %d to %s field", sz, structField.Name) + } + + mapV := fieldVal.MapIndex(mapK) + + cellVT := reflect.StructOf([]reflect.StructField{{ + Name: "Value", + Type: mapV.Type(), + Tag: reflect.StructTag(fmt.Sprintf("tlb:%q", strings.Join(settings[2:], " "))), + }}) + cellV := reflect.New(cellVT).Elem() + cellV.Field(0).Set(mapV) + + mapVC, err := toCell(cellV.Interface()) + if err != nil { + return fmt.Errorf("creating cell for dict value of '%s' field: %w", structField.Name, err) + } + + if err := dict.Set(mapKB.EndCell(), mapVC); err != nil { + return fmt.Errorf("set dict key/value on '%s' field: %w", structField.Name, err) + } + } + } + + if isInline { + dCell, err := dict.ToCell() + if err != nil { + return fmt.Errorf("failed to serialize inline dict to cell for %s, err: %w", structField.Name, err) + } + + if dCell == nil { + return fmt.Errorf("inline dict in field %s cannot be empty", structField.Name) + } + + if err = builder.StoreBuilder(dCell.ToBuilder()); err != nil { + return fmt.Errorf("failed to store inline dict for %s, err: %w", structField.Name, err) + } + } else { + if err := builder.StoreDict(dict); err != nil { + return fmt.Errorf("failed to store dict for %s, err: %w", structField.Name, err) + } + } + } else if settings[0] == "var" { + if settings[1] == "uint" { + sz, err := strconv.Atoi(settings[2]) + if err != nil { + panic(err.Error()) + } + + err = builder.StoreBigVarUInt(fieldVal.Interface().(*big.Int), uint(sz)) + if err != nil { + return fmt.Errorf("failed to store var uint: %w", err) + } + } else { + panic("var of type " + settings[1] + " is not supported") + } + } else { + panic(fmt.Sprintf("cannot serialize field '%s' as tag '%s', use manual serialization", structField.Name, structField.Tag.Get("tlb"))) + } + + if asRef { + err := root.StoreRef(builder.EndCell()) + if err != nil { + return fmt.Errorf("failed to store cell to ref for %s, err: %w", structField.Name, err) + } + } + + return nil +} + +func structStore(field reflect.Value, name string) (*cell.Cell, error) { + if field.Type() == cellType { + if field.IsNil() { + return cell.BeginCell().EndCell(), nil + } + return field.Interface().(*cell.Cell), nil + } + + inf := field.Interface() + + c, err := toCell(inf) + if err != nil { + return nil, fmt.Errorf("failed to store to cell for %s of type %s, err: %w", name, field.Type().String(), err) + } + return c, nil +} diff --git a/internal/app/fetcher/msg_hash/store_test.go b/internal/app/fetcher/msg_hash/store_test.go new file mode 100644 index 00000000..d98a3ea3 --- /dev/null +++ b/internal/app/fetcher/msg_hash/store_test.go @@ -0,0 +1,32 @@ +package msg_hash + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +func TestMessageHash(t *testing.T) { + boc, err := base64.StdEncoding.DecodeString("te6cckECBAEAAR0AA69oASMHseOMLtTOzNLikEih0deCMkOfpbEoKBf6paLY9GcjAD1oM7csliVgwVQWX2NJFMmuMwd7eKQC5qAsT7sqSWS/z6erQAYXhOIAAGrLlROtBNC3/X8bAQIDCEICuikYyJR+myWvmsG4gzV3VBc+WBL4B6PW5kKhRwlZU5UAhwCAG26EZAMiSZdaT9/nb07G7krKUuZrixwBQSSIrv7HC5zwAeNtMkLGaGxnMtFWA30ih6RtkEJcPo9X/7T7tSePPCP+AKkXjUUZAAAAAAAAAABLLQXgCAD/ZY5/Z2Ta6w8b6QBTfUEPDAQcImJeWM3qvgOeR8ZFMQAjz1KOv+3yBZDnGPDFi8WYoBUREP8nejEWbWd8wSf45cQF6MF87g==") + require.NoError(t, err) + + msgCell, err := cell.FromBOC(boc) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(msgCell.Hash()), "b3cfae95c4b916758edc6c8ca9c8ae837aeb40bd9981b4d9ca094358a36a780e") + + fmt.Println(hex.EncodeToString(msgCell.Hash())) + + msg := new(tlb.InternalMessage) + err = tlb.LoadFromCell(msg, msgCell.BeginParse()) + require.NoError(t, err) + + gotCell, err := toCell(tlb.Message{MsgType: tlb.MsgTypeInternal, Msg: msg}) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(gotCell.Hash()), "669e35112e147e5f88768d0e7c86482b1ce292e2ad7c4fbb977c651ea059ec1b") +} From 89d8d3b7c5345a9d6722b939d3bbb4efd5dfc905 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 20:44:43 +0300 Subject: [PATCH 107/120] [repo] message filter hash: lag for rounded max_lt --- internal/core/repository/msg/filter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 2d053a58..58b126c1 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -126,11 +126,11 @@ func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesR q. // query with filters Table("max_lt"). ColumnExpr("count(*) as v"). - Where("created_lt <= floor(max_lt.v, -7)"), // we round LT as messages in new blocks can have lower LT + Where("created_lt <= floor(max_lt.v, -7) - 1e7"), // we round LT as messages in new blocks can have lower LT ). Table("max_lt", "rounded_count"). ColumnExpr("max_lt.v AS max_lt_value"). - ColumnExpr("floor(max_lt.v, -7) as max_lt_rounded"). + ColumnExpr("floor(max_lt.v, -7) - 1e7 as max_lt_rounded"). ColumnExpr("rounded_count.v AS count") if err := q.Scan(ctx, &result); err != nil { @@ -156,7 +156,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag "rounded_max_lt", r.pg.NewSelect(). Model((*core.Message)(nil)). - ColumnExpr("floor(max(created_lt) / 1e7) * 1e7 AS v"), // we round LT as messages in new blocks can have lower LT + ColumnExpr("floor(max(created_lt) / 1e7) * 1e7 - 1e7 AS v"), // we round LT as messages in new blocks can have lower LT ). With( "until_rounded_count", From 8e6a81f72993ae6fd15e6f8803dbc23f7ffaae72 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 21:00:07 +0300 Subject: [PATCH 108/120] .golangci.yaml: do not check msg_hash folder --- .golangci.yaml | 1 + internal/app/fetcher/msg_hash/store_test.go | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index bb21fac7..733ebf6a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -93,6 +93,7 @@ linters: - third_party$ - builtin$ - examples$ + - internal/app/fetcher/msg_hash/store.go formatters: enable: - gofmt diff --git a/internal/app/fetcher/msg_hash/store_test.go b/internal/app/fetcher/msg_hash/store_test.go index d98a3ea3..e323acab 100644 --- a/internal/app/fetcher/msg_hash/store_test.go +++ b/internal/app/fetcher/msg_hash/store_test.go @@ -3,7 +3,6 @@ package msg_hash import ( "encoding/base64" "encoding/hex" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -20,8 +19,6 @@ func TestMessageHash(t *testing.T) { require.NoError(t, err) require.Equal(t, hex.EncodeToString(msgCell.Hash()), "b3cfae95c4b916758edc6c8ca9c8ae837aeb40bd9981b4d9ca094358a36a780e") - fmt.Println(hex.EncodeToString(msgCell.Hash())) - msg := new(tlb.InternalMessage) err = tlb.LoadFromCell(msg, msgCell.BeginParse()) require.NoError(t, err) From 625bc80545b721ddfe0840db71e32b1d68b22b14 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 21:52:23 +0300 Subject: [PATCH 109/120] [rndm] unify lt counter for tests --- internal/core/rndm/account.go | 5 ++--- internal/core/rndm/msg.go | 11 +++++------ internal/core/rndm/rndm.go | 2 ++ internal/core/rndm/tx.go | 7 +++---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/internal/core/rndm/account.go b/internal/core/rndm/account.go index c6ef5587..f2df3c1b 100644 --- a/internal/core/rndm/account.go +++ b/internal/core/rndm/account.go @@ -12,7 +12,6 @@ import ( var ( contractNames = []abi.ContractName{known.NFTCollection, known.NFTItem, known.JettonMinter, known.JettonWallet, "wallet_v3r1", "wallet_v4r2"} - lastTxLT uint64 timestamp = time.Now().UTC() ) @@ -32,7 +31,7 @@ func ContractNames(a *addr.Address) (ret []abi.ContractName) { } func AddressState(a *addr.Address, t []abi.ContractName, minter *addr.Address) *core.AccountState { - lastTxLT++ + lastLT++ timestamp = timestamp.Add(time.Minute) b := Block(0) @@ -45,7 +44,7 @@ func AddressState(a *addr.Address, t []abi.ContractName, minter *addr.Address) * IsActive: true, Status: core.Active, Balance: BigInt(), - LastTxLT: lastTxLT, + LastTxLT: lastLT, LastTxHash: Bytes(32), StateHash: Bytes(32), Code: Bytes(32), diff --git a/internal/core/rndm/msg.go b/internal/core/rndm/msg.go index 9b1e47be..be7fb799 100644 --- a/internal/core/rndm/msg.go +++ b/internal/core/rndm/msg.go @@ -11,8 +11,7 @@ import ( var ( // operationNames = []string{"nft_item_transfer", "nft_collection_item_mint"} - msgLT uint64 = 1000 - msgTS = time.Now().UTC() + msgTS = time.Now().UTC() ) // func OperationName() string { @@ -20,7 +19,7 @@ var ( // } func MessageFromTo(from, to *addr.Address) *core.Message { - msgLT++ + lastLT++ msgTS = msgTS.Add(time.Minute) src, dst := Block(0), Block(0) @@ -32,12 +31,12 @@ func MessageFromTo(from, to *addr.Address) *core.Message { SrcWorkchain: src.Workchain, SrcShard: src.Shard, SrcBlockSeqNo: src.SeqNo, - SrcTxLT: msgLT, + SrcTxLT: lastLT, DstAddress: *to, DstWorkchain: dst.Workchain, DstShard: dst.Shard, DstBlockSeqNo: dst.SeqNo, - DstTxLT: msgLT, + DstTxLT: lastLT, Amount: BigInt(), IHRFee: BigInt(), FwdFee: BigInt(), @@ -49,7 +48,7 @@ func MessageFromTo(from, to *addr.Address) *core.Message { StateInitCode: Bytes(64), StateInitData: Bytes(64), CreatedAt: msgTS, - CreatedLT: msgLT, + CreatedLT: lastLT, } } diff --git a/internal/core/rndm/rndm.go b/internal/core/rndm/rndm.go index 44caea9b..a77b2ef4 100644 --- a/internal/core/rndm/rndm.go +++ b/internal/core/rndm/rndm.go @@ -10,6 +10,8 @@ import ( "github.com/tonindexer/anton/addr" ) +var lastLT uint64 = 58717889000001 + func init() { rand.Seed(time.Now().UnixNano()) //nolint:staticcheck // TODO: migrate to a local random generator } diff --git a/internal/core/rndm/tx.go b/internal/core/rndm/tx.go index 2c9d0b66..e54831d0 100644 --- a/internal/core/rndm/tx.go +++ b/internal/core/rndm/tx.go @@ -9,13 +9,12 @@ import ( ) var ( - txTS = time.Now().UTC() - txLT uint64 = 80000 + txTS = time.Now().UTC() ) func BlockTransaction(b core.BlockID) *core.Transaction { txTS = txTS.Add(time.Minute) - txLT++ + lastLT++ return &core.Transaction{ Address: *Address(), @@ -35,7 +34,7 @@ func BlockTransaction(b core.BlockID) *core.Transaction { OrigStatus: core.Active, EndStatus: core.Active, CreatedAt: txTS, - CreatedLT: txLT, + CreatedLT: lastLT, } } From 4b59dd8e3bf6224d8fad3d5e0b7817ff834e0323 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 21:53:04 +0300 Subject: [PATCH 110/120] [repo] message filter counting: fix tests and since_start_count in partial scan --- internal/core/repository/msg/filter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 58b126c1..29d8a47b 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -130,7 +130,7 @@ func (r *Repository) countMsgFullScan(ctx context.Context, req *filter.MessagesR ). Table("max_lt", "rounded_count"). ColumnExpr("max_lt.v AS max_lt_value"). - ColumnExpr("floor(max_lt.v, -7) - 1e7 as max_lt_rounded"). + ColumnExpr("max2(floor(max_lt.v, -7) - 1e7, 0) as max_lt_rounded"). ColumnExpr("rounded_count.v AS count") if err := q.Scan(ctx, &result); err != nil { @@ -156,7 +156,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag "rounded_max_lt", r.pg.NewSelect(). Model((*core.Message)(nil)). - ColumnExpr("floor(max(created_lt) / 1e7) * 1e7 - 1e7 AS v"), // we round LT as messages in new blocks can have lower LT + ColumnExpr("greatest(floor(max(created_lt) / 1e7) * 1e7 - 1e7, 0) AS v"), // we round LT as messages in new blocks can have lower LT ). With( "until_rounded_count", @@ -175,7 +175,7 @@ func (r *Repository) countMsgPartialScan(ctx context.Context, req *filter.Messag &req.MessagesFilter, ). ColumnExpr("count(*) as v"). - Where("created_lt >= ?", startLt)). + Where("created_lt > ?", startLt)). Table("rounded_max_lt", "until_rounded_count", "since_start_count"). ColumnExpr("since_start_count.v AS since_start_count"). ColumnExpr("until_rounded_count.v AS until_rounded_count"). From 9cbc6dff95f9f28ca6bada3e30ad48e541e41feb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 21:57:30 +0300 Subject: [PATCH 111/120] [repo] countAccountStates: use rounded counting --- internal/core/repository/account/filter.go | 143 ++++++++++++------ .../core/repository/account/filter_test.go | 13 ++ 2 files changed, 107 insertions(+), 49 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 09611fda..b1e69f32 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -186,15 +186,15 @@ func (r *Repository) countAccountStatesFullScan(ctx context.Context, f *filter.A // For latest account states, we need to count distinct addresses q = r.ch.NewSelect(). Model((*core.AccountState)(nil)). - ColumnExpr("count(distinct address) AS count"). - ColumnExpr("(SELECT max(last_tx_lt) FROM account_states) AS max_lt") // unfiltered max + ColumnExpr("count(distinct address) AS count") } else { // For historical account states, we count all records q = r.ch.NewSelect(). Model((*core.AccountState)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(SELECT max(last_tx_lt) FROM account_states) AS max_lt") // unfiltered max + ColumnExpr("count(*) AS count") } + q = q.ColumnExpr("floor((SELECT max(last_tx_lt) AS v FROM account_states), -7) - 1e7 as max_lt") // unfiltered max + q = q.Where("last_tx_lt <= max_lt") if len(f.Addresses) > 0 { q = q.Where("address in (?)", ch.In(f.Addresses)) @@ -237,48 +237,14 @@ func (r *Repository) countAccountStatesFullScan(ctx context.Context, f *filter.A return result.Count, *result.MaxLT, nil } -func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *filter.AccountsReq, startLt uint64) (partialCount int, maxLt uint64, err error) { - var result struct { - Count int - MaxLT uint64 `bun:"max_lt"` - } - - var q *bun.SelectQuery - if req.LatestState { - q = r.pg.NewSelect(). - Model((*core.LatestAccountState)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(select max(last_tx_lt) from latest_account_states) AS max_lt") - if startLt > 0 { - q = q.Where("created_lt > ?", startLt) - } - } else { - q = r.pg.NewSelect(). - Model((*core.AccountState)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(select max(last_tx_lt) from account_states) AS max_lt"). - Where("last_tx_lt > ?", startLt) - } +func (r *Repository) countLatestAccountStatesFullScanFiltered(ctx context.Context, req *filter.AccountsReq) (count int, err error) { + q := r.pg.NewSelect().Model((*core.LatestAccountState)(nil)). + ColumnExpr("count(*) AS count") if len(req.Addresses) > 0 { q = q.Where("address in (?)", bun.In(req.Addresses)) } - if !req.LatestState { - if req.Workchain != nil { - q = q.Where("workchain = ?", *req.Workchain) - } - if req.Shard != nil { - q = q.Where("shard = ?", *req.Shard) - } - if req.BlockSeqNoLeq != nil { - q = q.Where("block_seq_no <= ?", *req.BlockSeqNoLeq) - } - if req.BlockSeqNoBeq != nil { - q = q.Where("block_seq_no >= ?", *req.BlockSeqNoBeq) - } - } - if len(req.ContractTypes) > 0 { q = q.Where("types && ?", pgdialect.Array(req.ContractTypes)) } @@ -289,20 +255,99 @@ func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *fil q = q.Where("minter_address = ?", req.MinterAddress) } - if err := q.Scan(ctx, &result); err != nil { - return 0, 0, err + if err := q.Scan(ctx, &count); err != nil { + return 0, err + } + + return count, nil +} + +func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *filter.AccountsReq, startLt uint64) (partialCount, roundedCount int, roundedMaxLt uint64, err error) { + var result struct { + SinceStartCount int `bun:"since_start_count"` + RoundedCount int `bun:"until_rounded_count"` + RoundedMaxLT uint64 `bun:"rounded_max_lt_value"` + } + + selectTable := func() *bun.SelectQuery { + if req.LatestState { + return r.pg.NewSelect().Model((*core.LatestAccountState)(nil)) + } else { + return r.pg.NewSelect().Model((*core.AccountState)(nil)) + } + } + ltColumn := func() string { + if req.LatestState { + return "created_lt" + } else { + return "last_tx_lt" + } } + applyFilters := func(q *bun.SelectQuery) *bun.SelectQuery { + if len(req.Addresses) > 0 { + q = q.Where("address in (?)", bun.In(req.Addresses)) + } + if !req.LatestState { + if req.Workchain != nil { + q = q.Where("workchain = ?", *req.Workchain) + } + if req.Shard != nil { + q = q.Where("shard = ?", *req.Shard) + } + if req.BlockSeqNoLeq != nil { + q = q.Where("block_seq_no <= ?", *req.BlockSeqNoLeq) + } + if req.BlockSeqNoBeq != nil { + q = q.Where("block_seq_no >= ?", *req.BlockSeqNoBeq) + } + } + if len(req.ContractTypes) > 0 { + q = q.Where("types && ?", pgdialect.Array(req.ContractTypes)) + } + if req.OwnerAddress != nil { + q = q.Where("owner_address = ?", req.OwnerAddress) + } + if req.MinterAddress != nil { + q = q.Where("minter_address = ?", req.MinterAddress) + } + return q + } + + q := r.pg.NewSelect(). + With( + "rounded_max_lt", + selectTable(). + ColumnExpr(fmt.Sprintf("floor(max(%s) / 1e7) * 1e7 - 1e7 AS v", ltColumn())), + ). + With( + "until_rounded_count", + applyFilters(selectTable()). + Table("rounded_max_lt"). + ColumnExpr("count(*) as v"). + Where(ltColumn()+" > ?", startLt). + Where(ltColumn()+" <= rounded_max_lt.v"), + ). + With( + "since_start_count", + applyFilters(selectTable()). + ColumnExpr("count(*) as v"). + Where(ltColumn()+" > ?", startLt), + ). + Table("rounded_max_lt", "until_rounded_count", "since_start_count"). + ColumnExpr("since_start_count.v AS since_start_count"). + ColumnExpr("until_rounded_count.v AS until_rounded_count"). + ColumnExpr("rounded_max_lt.v as rounded_max_lt_value") - if result.MaxLT == 0 { - result.MaxLT = startLt // no new rows + if err := q.Scan(ctx, &result); err != nil { + return 0, 0, 0, err } - return result.Count, result.MaxLT, nil + return result.SinceStartCount, result.RoundedCount, result.RoundedMaxLT, nil } func (r *Repository) countAccountStates(ctx context.Context, req *filter.AccountsReq) (int, error) { if req.LatestState && (len(req.ContractTypes) > 0 || req.OwnerAddress != nil || req.MinterAddress != nil) { - count, _, err := r.countAccountStatesPartialScan(ctx, req, 0) + count, err := r.countLatestAccountStatesFullScanFiltered(ctx, req) return count, err } @@ -331,11 +376,11 @@ func (r *Repository) countAccountStates(ctx context.Context, req *filter.Account } // get partial count since last cached value - partialCount, maxLT, err := r.countAccountStatesPartialScan(ctx, req, maxLT) + partialCount, roundedPartialCount, roundedMaxLT, err := r.countAccountStatesPartialScan(ctx, req, maxLT) if err != nil { return 0, err } - if err := cache.Set(req.AccountsFilter, count+partialCount, maxLT); err != nil { + if err := cache.Set(req.AccountsFilter, count+roundedPartialCount, roundedMaxLT); err != nil { return 0, err } diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index a93b4e87..9ab97ade 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -250,6 +250,19 @@ func TestRepository_FilterAccounts(t *testing.T) { require.Equal(t, []*core.AccountState{specialState}, results.Rows) }) + t.Run("filter states by non-existing contract types", func(t *testing.T) { + results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ + WithCodeData: true, + AccountsFilter: filter.AccountsFilter{ + ContractTypes: []abi.ContractName{"some_nonsense"}, + }, + Order: "DESC", Limit: 1, Count: true, + }) + require.Nil(t, err) + require.Equal(t, 0, results.Total) + require.Equal(t, []*core.AccountState(nil), results.Rows) + }) + t.Run("filter states by minter", func(t *testing.T) { results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ WithCodeData: true, From c451cf27c12872e5b7bb5128830070088d9326ab Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 22:13:04 +0300 Subject: [PATCH 112/120] [repo] account filter counting: remove countLatestAccountStatesFullScanFiltered --- internal/core/repository/account/filter.go | 30 ---------------------- 1 file changed, 30 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index b1e69f32..cfb3b104 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -237,31 +237,6 @@ func (r *Repository) countAccountStatesFullScan(ctx context.Context, f *filter.A return result.Count, *result.MaxLT, nil } -func (r *Repository) countLatestAccountStatesFullScanFiltered(ctx context.Context, req *filter.AccountsReq) (count int, err error) { - q := r.pg.NewSelect().Model((*core.LatestAccountState)(nil)). - ColumnExpr("count(*) AS count") - - if len(req.Addresses) > 0 { - q = q.Where("address in (?)", bun.In(req.Addresses)) - } - - if len(req.ContractTypes) > 0 { - q = q.Where("types && ?", pgdialect.Array(req.ContractTypes)) - } - if req.OwnerAddress != nil { - q = q.Where("owner_address = ?", req.OwnerAddress) - } - if req.MinterAddress != nil { - q = q.Where("minter_address = ?", req.MinterAddress) - } - - if err := q.Scan(ctx, &count); err != nil { - return 0, err - } - - return count, nil -} - func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *filter.AccountsReq, startLt uint64) (partialCount, roundedCount int, roundedMaxLt uint64, err error) { var result struct { SinceStartCount int `bun:"since_start_count"` @@ -346,11 +321,6 @@ func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *fil } func (r *Repository) countAccountStates(ctx context.Context, req *filter.AccountsReq) (int, error) { - if req.LatestState && (len(req.ContractTypes) > 0 || req.OwnerAddress != nil || req.MinterAddress != nil) { - count, err := r.countLatestAccountStatesFullScanFiltered(ctx, req) - return count, err - } - // choose the appropriate cache based on whether we're querying latest or historical states cache := r.statesFilterCountCache if req.LatestState { From 7c4bc6630ebe00d7644e06897fcec1099ecadf98 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 22:16:25 +0300 Subject: [PATCH 113/120] [repo] countAccountStatesFullScan: remove filter check --- internal/core/repository/account/filter.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index cfb3b104..383da774 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -172,10 +172,6 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts } func (r *Repository) countAccountStatesFullScan(ctx context.Context, f *filter.AccountsReq) (count int, maxLt uint64, err error) { - if f.LatestState && (len(f.ContractTypes) > 0 || f.MinterAddress != nil || f.OwnerAddress != nil) { - return 0, 0, errors.New("clickhouse latest account states full scan is not supported for these filters") - } - var result struct { Count int MaxLT *uint64 `ch:"max_lt"` From caa6129545b65d7210ed472e2161b39e5f6e75fd Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 25 Jun 2025 22:51:13 +0300 Subject: [PATCH 114/120] [repo] countAccountStates: full scan postgresql instead of clickhouse --- internal/core/repository/account/filter.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 383da774..f589ad68 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -288,7 +288,7 @@ func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *fil With( "rounded_max_lt", selectTable(). - ColumnExpr(fmt.Sprintf("floor(max(%s) / 1e7) * 1e7 - 1e7 AS v", ltColumn())), + ColumnExpr(fmt.Sprintf("greatest(floor(max(%s) / 1e7) * 1e7 - 1e7, 0) AS v", ltColumn())), ). With( "until_rounded_count", @@ -313,6 +313,10 @@ func (r *Repository) countAccountStatesPartialScan(ctx context.Context, req *fil return 0, 0, 0, err } + if result.RoundedMaxLT == 0 { + return 0, 0, 0, core.ErrNotFound + } + return result.SinceStartCount, result.RoundedCount, result.RoundedMaxLT, nil } @@ -327,7 +331,11 @@ func (r *Repository) countAccountStates(ctx context.Context, req *filter.Account count, maxLT, err := cache.Get(req.AccountsFilter) if errors.Is(err, core.ErrNotFound) { // full scan for initial count - count, maxLT, err = r.countAccountStatesFullScan(ctx, req) + if req.LatestState && (len(req.Addresses) > 0 || len(req.ContractTypes) > 0 || req.OwnerAddress != nil || req.MinterAddress != nil) { + _, count, maxLT, err = r.countAccountStatesPartialScan(ctx, req, 0) // full scan PostgreSQL table instead of Clickhouse + } else { + count, maxLT, err = r.countAccountStatesFullScan(ctx, req) + } if errors.Is(err, core.ErrNotFound) { return 0, nil } From 7f74263ce07c8091e9c6ea489d560f0a1bce726e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 26 Jun 2025 13:17:55 +0300 Subject: [PATCH 115/120] [repo] tx filter count: round max lt --- internal/core/repository/tx/filter.go | 94 +++++++++++++++++++-------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/internal/core/repository/tx/filter.go b/internal/core/repository/tx/filter.go index bfd4df24..fd22bc04 100644 --- a/internal/core/repository/tx/filter.go +++ b/internal/core/repository/tx/filter.go @@ -78,14 +78,13 @@ func (r *Repository) filterTx(ctx context.Context, req *filter.TransactionsReq) func (r *Repository) countTxFullScan(ctx context.Context, req *filter.TransactionsReq) (count int, maxLt uint64, err error) { var result struct { - Count int - MaxLT *uint64 `ch:"max_lt"` + Count int + MaxLT uint64 `ch:"max_lt_value"` + RoundedMaxLT uint64 `ch:"max_lt_rounded"` } q := r.ch.NewSelect(). - Model((*core.Transaction)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("(SELECT max(created_lt) FROM transactions) AS max_lt") // unfiltered max + Model((*core.Transaction)(nil)) if len(req.Hash) > 0 { q = q.Where("hash = ?", req.Hash) @@ -108,43 +107,86 @@ func (r *Repository) countTxFullScan(ctx context.Context, req *filter.Transactio q = q.Where("created_lt = ?", *req.CreatedLT) } + q = r.ch.NewSelect(). + With( + "max_lt", + r.ch.NewSelect(). + Model((*core.Transaction)(nil)). + ColumnExpr("max(created_lt) AS v"), + ). + With( + "rounded_count", + q. // query with filters + Table("max_lt"). + ColumnExpr("count(*) as v"). + Where("created_lt <= floor(max_lt.v, -7) - 1e7"), + ). + Table("max_lt", "rounded_count"). + ColumnExpr("max_lt.v AS max_lt_value"). + ColumnExpr("max2(floor(max_lt.v, -7) - 1e7, 0) as max_lt_rounded"). + ColumnExpr("rounded_count.v AS count") + if err := q.Scan(ctx, &result); err != nil { return 0, 0, err } - if result.MaxLT == nil { + if result.MaxLT == 0 { return 0, 0, core.ErrNotFound } - return result.Count, *result.MaxLT, nil + return result.Count, result.RoundedMaxLT, nil } -func (r *Repository) countTxPartialScan(ctx context.Context, req *filter.TransactionsReq, startLt uint64) (partialCount int, maxLt uint64, err error) { +func (r *Repository) countTxPartialScan(ctx context.Context, req *filter.TransactionsReq, startLt uint64) (partialCount, roundedCount int, roundedMaxLt uint64, err error) { var result struct { - Count int - MaxLT uint64 `bun:"max_lt"` + SinceStartCount int `bun:"since_start_count"` + RoundedCount int `bun:"until_rounded_count"` + RoundedMaxLT uint64 `bun:"rounded_max_lt_value"` } q := r.pg.NewSelect(). - Model((*core.Transaction)(nil)). - ColumnExpr("count(*) AS count"). - ColumnExpr("COALESCE(max(created_lt), ?) AS max_lt", startLt). - Where("created_lt > ?", startLt) - - q = r.getFilterTxQuery(q, &req.TransactionsFilter) + With( + "rounded_max_lt", + r.pg.NewSelect(). + Model((*core.Transaction)(nil)). + ColumnExpr("greatest(floor(max(created_lt) / 1e7) * 1e7 - 1e7, 0) AS v"), // we round LT as transactions in new blocks can have lower LT + ). + With( + "until_rounded_count", + r.getFilterTxQuery( + r.pg.NewSelect().Model((*core.Transaction)(nil)), + &req.TransactionsFilter, + ). + Table("rounded_max_lt"). + ColumnExpr("count(*) as v"). + Where("created_lt > ?", startLt). + Where("created_lt <= rounded_max_lt.v"), + ). + With("since_start_count", + r.getFilterTxQuery( + r.pg.NewSelect().Model((*core.Transaction)(nil)), + &req.TransactionsFilter, + ). + ColumnExpr("count(*) as v"). + Where("created_lt > ?", startLt)). + Table("rounded_max_lt", "until_rounded_count", "since_start_count"). + ColumnExpr("since_start_count.v AS since_start_count"). + ColumnExpr("until_rounded_count.v AS until_rounded_count"). + ColumnExpr("rounded_max_lt.v as rounded_max_lt_value") if err := q.Scan(ctx, &result); err != nil { - return 0, 0, err - } - - if result.MaxLT == 0 { - result.MaxLT = startLt // no new rows + return 0, 0, 0, err } - return result.Count, result.MaxLT, nil + return result.SinceStartCount, result.RoundedCount, result.RoundedMaxLT, nil } func (r *Repository) countTx(ctx context.Context, req *filter.TransactionsReq) (int, error) { + if len(req.Hash) > 0 || req.BlockID != nil || req.CreatedLT != nil { // count value cannot change on any of these filters + count, _, _, err := r.countTxPartialScan(ctx, req, 0) + return count, err + } + count, maxLT, err := r.transactionsFilterCountCache.Get(req.TransactionsFilter) if errors.Is(err, core.ErrNotFound) { count, maxLT, err = r.countTxFullScan(ctx, req) @@ -161,15 +203,11 @@ func (r *Repository) countTx(ctx context.Context, req *filter.TransactionsReq) ( return 0, err } - if len(req.Hash) > 0 || req.BlockID != nil || req.CreatedLT != nil { - return count, nil // count value cannot change on any of these filters - } - - partialCount, maxLT, err := r.countTxPartialScan(ctx, req, maxLT) + partialCount, roundedPartialCount, roundedMaxLT, err := r.countTxPartialScan(ctx, req, maxLT) if err != nil { return 0, err } - if err := r.transactionsFilterCountCache.Set(req.TransactionsFilter, count+partialCount, maxLT); err != nil { + if err := r.transactionsFilterCountCache.Set(req.TransactionsFilter, count+roundedPartialCount, roundedMaxLT); err != nil { return 0, err } From 5a2bce56617a6832da3f89670ec6567ed94dd66e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 26 Jun 2025 13:20:42 +0300 Subject: [PATCH 116/120] [repo] countAccountStates: nolint nestif --- internal/core/repository/account/filter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index f589ad68..8c59e81d 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -329,7 +329,7 @@ func (r *Repository) countAccountStates(ctx context.Context, req *filter.Account // try to get from cache count, maxLT, err := cache.Get(req.AccountsFilter) - if errors.Is(err, core.ErrNotFound) { + if errors.Is(err, core.ErrNotFound) { //nolint:nestif // cache entry is not found, we do full scan first // full scan for initial count if req.LatestState && (len(req.Addresses) > 0 || len(req.ContractTypes) > 0 || req.OwnerAddress != nil || req.MinterAddress != nil) { _, count, maxLT, err = r.countAccountStatesPartialScan(ctx, req, 0) // full scan PostgreSQL table instead of Clickhouse From b498510ecdb214f25d8d36389a451167f7c5ac73 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 26 Jun 2025 17:46:55 +0300 Subject: [PATCH 117/120] [core] latest account states: add fake flag --- internal/core/account.go | 2 ++ internal/core/repository/account/account.go | 2 ++ ...50620183211_latest_parsed_account_states.up.sql | 14 +++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index 93019e18..db1588ae 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -140,6 +140,8 @@ type LatestAccountState struct { OwnerAddress *addr.Address `bun:"type:bytea" json:"owner_address,omitempty"` // universal column for many contracts MinterAddress *addr.Address `bun:"type:bytea" json:"minter_address,omitempty"` + Fake bool `bun:"type:boolean" json:"fake"` + CreatedLT uint64 `bun:"type:bigint,notnull" json:"created_lt"` AccountState *AccountState `bun:"rel:has-one,join:address=address,join:last_tx_lt=last_tx_lt" json:"account"` diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 10863600..e7f97977 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -284,6 +284,7 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ Set("types = EXCLUDED.types"). Set("owner_address = EXCLUDED.owner_address"). Set("minter_address = EXCLUDED.minter_address"). + Set("fake = EXCLUDED.fake"). Exec(ctx) if err != nil { return errors.Wrapf(err, "cannot set latest state for %s", &a) @@ -357,6 +358,7 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A Set("types = ?types"). Set("owner_address = ?owner_address"). Set("minter_address = ?minter_address"). + Set("fake = ?fake"). Where("address = ?address"). Where("last_tx_lt = ?last_tx_lt"). Exec(ctx) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql index c90535a5..7fe32b9a 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.up.sql @@ -3,7 +3,8 @@ BEGIN; ALTER TABLE latest_account_states ADD COLUMN types text[], ADD COLUMN owner_address bytea, - ADD COLUMN minter_address bytea; + ADD COLUMN minter_address bytea, + ADD COLUMN fake boolean not null default false; CREATE INDEX latest_account_states_types_idx ON latest_account_states USING gin (types); CREATE INDEX latest_account_states_minter_address_idx ON latest_account_states USING btree (minter_address) WHERE (minter_address IS NOT NULL); @@ -28,13 +29,14 @@ COMMIT; -- LOOP -- -- Update the next batch using a subquery to select the limited rows first -- WITH batch_to_update AS ( --- SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address +-- SELECT s.address, s.last_tx_lt, s.types, s.owner_address, s.minter_address, s.fake -- FROM account_states s -- WHERE s.last_tx_lt >= last_processed_tx_lt -- AND ( -- s.types IS NOT NULL OR -- s.owner_address IS NOT NULL OR --- s.minter_address IS NOT NULL +-- s.minter_address IS NOT NULL OR +-- s.fake IS NOT NULL -- ) -- ORDER BY s.last_tx_lt -- LIMIT batch_size @@ -44,14 +46,16 @@ COMMIT; -- SET -- types = b.types, -- owner_address = b.owner_address, --- minter_address = b.minter_address +-- minter_address = b.minter_address, +-- fake = b.fake -- FROM batch_to_update b -- WHERE las.address = b.address -- AND las.last_tx_lt = b.last_tx_lt -- AND ( -- las.types IS DISTINCT FROM b.types OR -- las.owner_address IS DISTINCT FROM b.owner_address OR --- las.minter_address IS DISTINCT FROM b.minter_address +-- las.minter_address IS DISTINCT FROM b.minter_address OR +-- las.fake IS DISTINCT FROM b.fake -- ) -- RETURNING b.last_tx_lt -- ) From d6497190c789834ded673c02599c9caf158ca6c5 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 26 Jun 2025 20:05:31 +0300 Subject: [PATCH 118/120] [repo] AggregateAccounts: move queries to postgresql --- internal/core/aggregate/account.go | 2 +- internal/core/repository/account/aggregate.go | 100 ++++++++++-------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/internal/core/aggregate/account.go b/internal/core/aggregate/account.go index 17e07c7c..31364e9d 100644 --- a/internal/core/aggregate/account.go +++ b/internal/core/aggregate/account.go @@ -27,7 +27,7 @@ type AccountsRes struct { Items int `json:"items,omitempty"` OwnersCount int `json:"owners_count,omitempty"` OwnedItems []*struct { - OwnerAddress *addr.Address `ch:"type:String" json:"owner_address"` + OwnerAddress *addr.Address `json:"owner_address"` ItemsCount int `json:"items_count"` } `json:"owned_items,omitempty"` UniqueOwners []*struct { diff --git a/internal/core/repository/account/aggregate.go b/internal/core/repository/account/aggregate.go index 38d2ec20..3928eae2 100644 --- a/internal/core/repository/account/aggregate.go +++ b/internal/core/repository/account/aggregate.go @@ -5,11 +5,9 @@ import ( "database/sql" "github.com/pkg/errors" - "github.com/uptrace/go-clickhouse/ch" "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/abi/known" - "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/aggregate" ) @@ -17,7 +15,7 @@ import ( func (r *Repository) aggregateAddressStatistics(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { var err error - res.TransactionsCount, err = r.ch.NewSelect(). + res.TransactionsCount, err = r.pg.NewSelect(). Model((*core.Transaction)(nil)). Where("address = ?", req.Address). Count(ctx) @@ -29,10 +27,10 @@ func (r *Repository) aggregateAddressStatistics(ctx context.Context, req *aggreg Types []abi.ContractName Count int } - err = r.ch.NewSelect(). - Model((*core.AccountState)(nil)). + err = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). Column("types"). - ColumnExpr("uniqExact(address) as count"). + ColumnExpr("count(*) as count"). Where("owner_address = ?", req.Address). Group("types"). Scan(ctx, &countByInterfaces) @@ -56,33 +54,25 @@ func (r *Repository) aggregateAddressStatistics(ctx context.Context, req *aggreg return nil } -func (r *Repository) makeLastItemStateQuery(minter *addr.Address) *ch.SelectQuery { - return r.ch.NewSelect(). - Model((*core.AccountState)(nil)). - ColumnExpr("argMax(address, last_tx_lt) as item_address"). - Where("minter_address = ?", minter). - Where("fake = false"). - Group("address") -} - -func (r *Repository) makeLastItemOwnerQuery(minter *addr.Address) *ch.SelectQuery { - return r.makeLastItemStateQuery(minter). - ColumnExpr("argMax(owner_address, last_tx_lt) AS owner_address") -} - func (r *Repository) aggregateNFTMinter(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { var err error - res.Items, err = r.makeLastItemStateQuery(req.MinterAddress).Count(ctx) + res.Items, err = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + Where("minter_address = ?", req.MinterAddress). + Where("fake = false"). + Count(ctx) if err != nil { return errors.Wrap(err, "count nft items") } // TODO: owners include sale contracts - err = r.ch.NewSelect(). - ColumnExpr("uniqExact(owner_address)"). - TableExpr("(?) as q", r.makeLastItemOwnerQuery(req.MinterAddress)). + err = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + ColumnExpr("count(owner_address)"). + Where("minter_address = ?", req.MinterAddress). + Where("fake = false"). Scan(ctx, &res.OwnersCount) if err != nil { return errors.Wrap(err, "count owners of nft minter") @@ -93,6 +83,7 @@ func (r *Repository) aggregateNFTMinter(ctx context.Context, req *aggregate.Acco ColumnExpr("address AS item_address"). ColumnExpr("uniqExact(owner_address) AS owners_count"). Where("minter_address = ?", req.MinterAddress). + Where("fake = false"). Group("item_address"). Order("owners_count DESC"). Limit(req.Limit). @@ -101,10 +92,12 @@ func (r *Repository) aggregateNFTMinter(ctx context.Context, req *aggregate.Acco return errors.Wrap(err, "count unique owners of nft items") } - err = r.ch.NewSelect(). + err = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). ColumnExpr("owner_address"). - ColumnExpr("count(item_address) AS items_count"). - TableExpr("(?) as q", r.makeLastItemOwnerQuery(req.MinterAddress)). + ColumnExpr("count(address) as items_count"). + Where("minter_address = ?", req.MinterAddress). + Where("fake = false"). Group("owner_address"). Order("items_count DESC"). Limit(req.Limit). @@ -119,25 +112,46 @@ func (r *Repository) aggregateNFTMinter(ctx context.Context, req *aggregate.Acco func (r *Repository) aggregateFTMinter(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { var err error - res.Wallets, err = r.makeLastItemStateQuery(req.MinterAddress).Count(ctx) + res.Wallets, err = r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + Where("latest_account_state.minter_address = ?", req.MinterAddress). + Where("latest_account_state.fake = false"). + Count(ctx) if err != nil { return errors.Wrap(err, "count jetton wallets") } - err = r.ch.NewSelect(). - ColumnExpr("sum(balance) as total_supply"). + err = r.pg.NewSelect(). + ColumnExpr("sum(jetton_balance)"). TableExpr("(?) as q", - r.makeLastItemOwnerQuery(req.MinterAddress). - ColumnExpr("argMax(jetton_balance, last_tx_lt) AS balance")). + r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + Relation("AccountState"). + ColumnExpr("account_state.jetton_balance"). + Where("latest_account_state.minter_address = ?", req.MinterAddress). + Where("latest_account_state.fake = false"), + ). Scan(ctx, &res.TotalSupply) if err != nil { return errors.Wrap(err, "count jetton total supply") } - err = r.makeLastItemOwnerQuery(req.MinterAddress). - ColumnExpr("argMax(jetton_balance, last_tx_lt) AS balance"). - Order("balance DESC"). - Limit(req.Limit). + err = r.pg.NewSelect(). + ColumnExpr("wallet_address"). + ColumnExpr("owner_address"). + ColumnExpr("balance"). + TableExpr("(?) as q", + r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + Relation("AccountState"). + ColumnExpr("latest_account_state.address as wallet_address"). + ColumnExpr("latest_account_state.owner_address as owner_address"). + ColumnExpr("account_state.jetton_balance as balance"). + Where("latest_account_state.minter_address = ?", req.MinterAddress). + Where("latest_account_state.fake = false"). + Order("balance DESC"). + Limit(req.Limit), + ). Scan(ctx, &res.OwnedBalance) if err != nil { return errors.Wrap(err, "count jetton holders") @@ -147,14 +161,16 @@ func (r *Repository) aggregateFTMinter(ctx context.Context, req *aggregate.Accou } func (r *Repository) aggregateMinterStatistics(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { - var interfaces []abi.ContractName + var interfacesRes struct { + Interfaces []abi.ContractName `bun:"type:text[],array"` + } - err := r.ch.NewSelect(). - Model((*core.AccountState)(nil)). - ColumnExpr("argMax(types, last_tx_lt) as interfaces"). + err := r.pg.NewSelect(). + Model((*core.LatestAccountState)(nil)). + ColumnExpr("types as interfaces"). Where("address = ?", req.MinterAddress). Group("address"). - Scan(ctx, &interfaces) + Scan(ctx, &interfacesRes) if errors.Is(err, sql.ErrNoRows) { return nil } @@ -162,7 +178,7 @@ func (r *Repository) aggregateMinterStatistics(ctx context.Context, req *aggrega return err } - for _, t := range interfaces { + for _, t := range interfacesRes.Interfaces { switch t { case known.NFTCollection: if err := r.aggregateNFTMinter(ctx, req, res); err != nil { From bd505af9fbe5753c6fea174a08b0c4aa092eb0a9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 27 Jun 2025 18:40:55 +0300 Subject: [PATCH 119/120] README.md: prettify cgo lib export example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aba39a71..5f57880a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ go test -p 1 $(go list ./... | grep /abi) -covermode=count To run the tests you might need to provide a path to the emulator library. For example: ```shell -CGO_LDFLAGS="-L /Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -Wl,-rpath,/Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -l emulator" go test -p 1 $(go list ./... | grep /abi) -covermode=count +CGO_LDFLAGS="-L /Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -Wl,-rpath,/Users/user/go/src/github.com/tonkeeper/tongo/lib/darwin/ -l emulator" \ + go test -p 1 $(go list ./... | grep /abi) -covermode=count ``` Run repositories tests: From d7db10e221170f004305d1b401612c8bf5ad3d90 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 27 Jun 2025 19:10:34 +0300 Subject: [PATCH 120/120] [migrations] latest_account_states: drop fake column in down migration --- .../20250620183211_latest_parsed_account_states.down.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql index c9507505..79dc2725 100644 --- a/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql +++ b/migrations/pgmigrations/20250620183211_latest_parsed_account_states.down.sql @@ -5,6 +5,7 @@ BEGIN; ALTER TABLE latest_account_states DROP COLUMN types, DROP COLUMN owner_address, - DROP COLUMN minter_address; + DROP COLUMN minter_address, + DROP COLUMN fake; COMMIT;