From ca5a6f7f4179f9cda33687d3e84dc2a73444937d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 20 Jun 2025 19:33:23 +0300 Subject: [PATCH 01/55] [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 02/55] [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 03/55] [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 04/55] [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 05/55] [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 06/55] [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 07/55] [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 08/55] [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 09/55] [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 10/55] [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 11/55] [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 12/55] [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 13/55] [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 14/55] [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 15/55] [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 16/55] [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 17/55] [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 18/55] [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 19/55] [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 20/55] 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 21/55] 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 22/55] [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 23/55] [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 24/55] [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 25/55] [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 26/55] [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 27/55] [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 28/55] 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 29/55] 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 30/55] 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 31/55] [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 32/55] .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 33/55] 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 34/55] [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 35/55] [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 36/55] [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 37/55] [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 38/55] [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 39/55] [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 40/55] [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 41/55] 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 42/55] [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 43/55] [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 44/55] [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 45/55] [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 46/55] [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 47/55] .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 48/55] [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 49/55] [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 50/55] [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 51/55] [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 52/55] [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 53/55] [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 54/55] [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 55/55] [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