From d3f84b828cf053e38b97d4bbd25e50912177219f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 5 Oct 2023 17:04:32 +0530 Subject: [PATCH 001/186] indexer.uniqAccounts: filter out by tx lt and address --- internal/app/indexer/save.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index c8a817d9..e6d16cce 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -84,17 +84,22 @@ func (s *Service) insertData( func (s *Service) uniqAccounts(transactions []*core.Transaction) []*core.AccountState { var ret []*core.AccountState - uniqAcc := make(map[addr.Address]*core.AccountState) + uniqAcc := make(map[addr.Address]map[uint64]*core.AccountState) - for j := range transactions { - tx := transactions[j] - if tx.Account != nil { - uniqAcc[tx.Account.Address] = tx.Account + for _, tx := range transactions { + if tx.Account == nil { + continue } + if uniqAcc[tx.Account.Address] == nil { + uniqAcc[tx.Account.Address] = map[uint64]*core.AccountState{} + } + uniqAcc[tx.Account.Address][tx.Account.LastTxLT] = tx.Account } - for _, a := range uniqAcc { - ret = append(ret, a) + for _, accounts := range uniqAcc { + for _, a := range accounts { + ret = append(ret, a) + } } return ret From 7f7143b800e852ffec5d8ebdca0e7bc266febc2b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 5 Oct 2023 17:04:32 +0530 Subject: [PATCH 002/186] [indexer] uniqAccounts: filter out by tx lt and address --- internal/app/indexer/save.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/app/indexer/save.go b/internal/app/indexer/save.go index c8a817d9..e6d16cce 100644 --- a/internal/app/indexer/save.go +++ b/internal/app/indexer/save.go @@ -84,17 +84,22 @@ func (s *Service) insertData( func (s *Service) uniqAccounts(transactions []*core.Transaction) []*core.AccountState { var ret []*core.AccountState - uniqAcc := make(map[addr.Address]*core.AccountState) + uniqAcc := make(map[addr.Address]map[uint64]*core.AccountState) - for j := range transactions { - tx := transactions[j] - if tx.Account != nil { - uniqAcc[tx.Account.Address] = tx.Account + for _, tx := range transactions { + if tx.Account == nil { + continue } + if uniqAcc[tx.Account.Address] == nil { + uniqAcc[tx.Account.Address] = map[uint64]*core.AccountState{} + } + uniqAcc[tx.Account.Address][tx.Account.LastTxLT] = tx.Account } - for _, a := range uniqAcc { - ret = append(ret, a) + for _, accounts := range uniqAcc { + for _, a := range accounts { + ret = append(ret, a) + } } return ret From 03a45cda6e9cadf96e6b9ee47e4db711ffb7200b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 10 Oct 2023 19:43:22 +0530 Subject: [PATCH 003/186] [abi/known] rename dns test file --- abi/known/{dns_test.go => tep81_dns_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename abi/known/{dns_test.go => tep81_dns_test.go} (100%) diff --git a/abi/known/dns_test.go b/abi/known/tep81_dns_test.go similarity index 100% rename from abi/known/dns_test.go rename to abi/known/tep81_dns_test.go From 8dd808997894308df8ba5b1d37b77bb93e6a0548 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 10 Oct 2023 21:38:16 +0530 Subject: [PATCH 004/186] [repo] count block transactions (#26) --- api/http/docs.go | 3 + api/http/swagger.json | 3 + api/http/swagger.yaml | 2 + internal/core/block.go | 3 +- internal/core/repository/block/block_test.go | 62 +++++++++++------ internal/core/repository/block/filter.go | 68 ++++++++++++++++++- internal/core/repository/block/filter_test.go | 58 +++++++++++----- internal/core/repository/repository_test.go | 1 + internal/core/rndm/block.go | 4 +- internal/core/rndm/tx.go | 6 +- 10 files changed, 162 insertions(+), 48 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index 9ee5c189..3ebe70d7 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -1441,6 +1441,9 @@ const docTemplate = `{ "$ref": "#/definitions/core.Transaction" } }, + "transactions_count": { + "type": "integer" + }, "workchain": { "type": "integer" } diff --git a/api/http/swagger.json b/api/http/swagger.json index 2278bd26..81125f2c 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -1438,6 +1438,9 @@ "$ref": "#/definitions/core.Transaction" } }, + "transactions_count": { + "type": "integer" + }, "workchain": { "type": "integer" } diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 1d69c644..455bdf17 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -375,6 +375,8 @@ definitions: items: $ref: '#/definitions/core.Transaction' type: array + transactions_count: + type: integer workchain: type: integer type: object diff --git a/internal/core/block.go b/internal/core/block.go index 3ff9769c..47e2e9c9 100644 --- a/internal/core/block.go +++ b/internal/core/block.go @@ -37,7 +37,8 @@ type Block struct { MasterID *BlockID `ch:"-" bun:"embed:master_" json:"master,omitempty"` Shards []*Block `ch:"-" bun:"rel:has-many,join:workchain=master_workchain,join:shard=master_shard,join:seq_no=master_seq_no" json:"shards,omitempty"` - Transactions []*Transaction `ch:"-" bun:"rel:has-many,join:workchain=workchain,join:shard=shard,join:seq_no=block_seq_no" json:"transactions,omitempty"` + TransactionsCount int `ch:"-" bun:"transactions_count,scanonly" json:"transactions_count"` + Transactions []*Transaction `ch:"-" bun:"rel:has-many,join:workchain=workchain,join:shard=shard,join:seq_no=block_seq_no" json:"transactions,omitempty"` // TODO: block info data diff --git a/internal/core/repository/block/block_test.go b/internal/core/repository/block/block_test.go index 4aa1bed2..3b5d655c 100644 --- a/internal/core/repository/block/block_test.go +++ b/internal/core/repository/block/block_test.go @@ -3,10 +3,12 @@ package block_test import ( "context" "database/sql" + "strings" "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" @@ -14,6 +16,7 @@ import ( "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository/block" + "github.com/tonindexer/anton/internal/core/repository/tx" "github.com/tonindexer/anton/internal/core/rndm" ) @@ -34,28 +37,43 @@ func initdb(t testing.TB) { ck = ch.Connect(ch.WithDSN(dsnCH), ch.WithAutoCreateDatabase(true), ch.WithPoolSize(16)) err = ck.Ping(ctx) - assert.Nil(t, err) + require.Nil(t, err) pg = bun.NewDB(sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))), pgdialect.New()) err = pg.Ping() - assert.Nil(t, err) + require.Nil(t, err) repo = block.NewRepository(ck, pg) } func createTables(t testing.TB) { err := block.CreateTables(context.Background(), ck, pg) - assert.Nil(t, err) + require.Nil(t, err) + + _, err = pg.ExecContext(context.Background(), "CREATE TYPE account_status AS ENUM (?, ?, ?, ?)", + core.Uninit, core.Active, core.Frozen, core.NonExist) + require.False(t, err != nil && !strings.Contains(err.Error(), "already exists")) + + err = tx.CreateTables(context.Background(), ck, pg) + require.Nil(t, err) } func dropTables(t testing.TB) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - _, err := ck.NewDropTable().Model((*core.Block)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + _, err := ck.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + + _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS account_status") + require.Nil(t, err) + + _, err = ck.NewDropTable().Model((*core.Block)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Block)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) } func TestRepository_AddBlocks(t *testing.T) { @@ -81,32 +99,32 @@ func TestRepository_AddBlocks(t *testing.T) { }) t.Run("add block", func(t *testing.T) { - tx, err := pg.Begin() - assert.Nil(t, err) + dbTx, err := pg.Begin() + require.Nil(t, err) - err = repo.AddBlocks(ctx, tx, []*core.Block{master, shard}) - assert.Nil(t, err) + err = repo.AddBlocks(ctx, dbTx, []*core.Block{master, shard}) + require.Nil(t, err) got := new(core.Block) - err = tx.NewSelect().Model(got).Where("workchain = 0").Scan(ctx) - assert.Nil(t, err) - assert.Equal(t, shard, got) + err = dbTx.NewSelect().Model(got).Where("workchain = 0").Scan(ctx) + require.Nil(t, err) + require.Equal(t, shard, got) err = ck.NewSelect().Model(got).Where("workchain = 0").Scan(ctx) - assert.Nil(t, err) - assert.Equal(t, shard.ScannedAt.Truncate(time.Second), got.ScannedAt.UTC()) // TODO: debug ch timestamps + require.Nil(t, err) + require.Equal(t, shard.ScannedAt.Truncate(time.Second), got.ScannedAt.UTC()) // TODO: debug ch timestamps got.ScannedAt = shard.ScannedAt - assert.Equal(t, shard, got) + require.Equal(t, shard, got) - err = tx.Commit() - assert.Nil(t, err) + err = dbTx.Commit() + require.Nil(t, err) }) t.Run("get last masterchain block", func(t *testing.T) { b, err := repo.GetLastMasterBlock(ctx) - assert.Nil(t, err) - assert.Equal(t, master, b) + require.Nil(t, err) + require.Equal(t, master, b) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index b730e448..3769fd0c 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -28,6 +28,63 @@ func loadTransactions(q *bun.SelectQuery, prefix string, f *filter.BlocksReq) *b return q } +func (r *Repository) countTransactions(ctx context.Context, ret []*core.Block) error { + var blockIDs [][]int64 + for _, m := range ret { + for _, s := range m.Shards { + blockIDs = append(blockIDs, []int64{int64(s.Workchain), s.Shard, int64(s.SeqNo)}) + } + blockIDs = append(blockIDs, []int64{int64(m.Workchain), m.Shard, int64(m.SeqNo)}) + } + + var res []struct { + Workchain int32 + Shard int64 + SeqNo uint32 `bun:"block_seq_no"` + TransactionsCount int + } + err := r.pg.NewSelect(). + TableExpr("transactions AS tx"). + Column("workchain", "shard", "block_seq_no"). + ColumnExpr("COUNT(*) as transactions_count"). + Where("(workchain, shard, block_seq_no) IN (?)", bun.In(blockIDs)). + Group("workchain", "shard", "block_seq_no"). + Scan(ctx, &res) + if err != nil { + return err + } + + var counts = make(map[int32]map[int64]map[uint32]int) + for _, r := range res { + if counts[r.Workchain] == nil { + counts[r.Workchain] = map[int64]map[uint32]int{} + } + if counts[r.Workchain][r.Shard] == nil { + counts[r.Workchain][r.Shard] = map[uint32]int{} + } + counts[r.Workchain][r.Shard][r.SeqNo] = r.TransactionsCount + } + + getCount := func(workchain int32, shard int64, seqNo uint32) int { + if counts[workchain] == nil { + return 0 + } + if counts[workchain][shard] == nil { + return 0 + } + return counts[workchain][shard][seqNo] + } + + for _, m := range ret { + for _, s := range m.Shards { + s.TransactionsCount = getCount(s.Workchain, s.Shard, s.SeqNo) + } + m.TransactionsCount = getCount(m.Workchain, m.Shard, m.SeqNo) + } + + return nil +} + func (r *Repository) filterBlocks(ctx context.Context, f *filter.BlocksReq) (ret []*core.Block, err error) { q := r.pg.NewSelect().Model(&ret) @@ -72,8 +129,15 @@ func (r *Repository) filterBlocks(ctx context.Context, f *filter.BlocksReq) (ret } q = q.Limit(f.Limit) - err = q.Scan(ctx) - return ret, err + if err := q.Scan(ctx); err != nil { + return nil, err + } + + if err = r.countTransactions(ctx, ret); err != nil { + return nil, err + } + + return ret, nil } func (r *Repository) countBlocks(ctx context.Context, f *filter.BlocksReq) (int, error) { diff --git a/internal/core/repository/block/filter_test.go b/internal/core/repository/block/filter_test.go index f8d6aab6..4b36cd6c 100644 --- a/internal/core/repository/block/filter_test.go +++ b/internal/core/repository/block/filter_test.go @@ -5,10 +5,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/filter" + "github.com/tonindexer/anton/internal/core/repository/tx" "github.com/tonindexer/anton/internal/core/rndm" ) @@ -19,6 +20,7 @@ func TestRepository_FilterBlocks(t *testing.T) { defer cancel() master := rndm.MasterBlock() + master.TransactionsCount = 10 shards := rndm.Blocks(0, 100) shard := shards[len(shards)-1] @@ -27,6 +29,7 @@ func TestRepository_FilterBlocks(t *testing.T) { Shard: master.Shard, SeqNo: master.SeqNo, } + shard.TransactionsCount = 20 master.Shards = append(master.Shards, shard) @@ -41,17 +44,36 @@ func TestRepository_FilterBlocks(t *testing.T) { }) t.Run("add block", func(t *testing.T) { - tx, err := pg.Begin() - assert.Nil(t, err) + dbTx, err := pg.Begin() + require.Nil(t, err) - err = repo.AddBlocks(ctx, tx, shards) - assert.Nil(t, err) + err = repo.AddBlocks(ctx, dbTx, shards) + require.Nil(t, err) - err = repo.AddBlocks(ctx, tx, []*core.Block{master}) - assert.Nil(t, err) + err = repo.AddBlocks(ctx, dbTx, []*core.Block{master}) + require.Nil(t, err) - err = tx.Commit() - assert.Nil(t, err) + err = dbTx.Commit() + require.Nil(t, err) + }) + + t.Run("add some transactions", func(t *testing.T) { + txRepo := tx.NewRepository(ck, pg) + + dbTx, err := pg.Begin() + require.Nil(t, err) + + masterTx := rndm.BlockTransactions(*shard.MasterID, master.TransactionsCount) + shardTx := rndm.BlockTransactions(shard.ID(), shard.TransactionsCount) + + err = txRepo.AddTransactions(ctx, dbTx, masterTx) + require.Nil(t, err) + + err = txRepo.AddTransactions(ctx, dbTx, shardTx) + require.Nil(t, err) + + err = dbTx.Commit() + require.Nil(t, err) }) t.Run("filter by workchain", func(t *testing.T) { @@ -62,9 +84,9 @@ func TestRepository_FilterBlocks(t *testing.T) { AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 100, res.Total) - assert.Equal(t, []*core.Block{shard}, res.Rows) + require.Nil(t, err) + require.Equal(t, 100, res.Total) + require.Equal(t, []*core.Block{shard}, res.Rows) }) t.Run("filter by seq no", func(t *testing.T) { @@ -74,9 +96,9 @@ func TestRepository_FilterBlocks(t *testing.T) { AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, []*core.Block{shard}, res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, []*core.Block{shard}, res.Rows) }) t.Run("filter by file hash", func(t *testing.T) { @@ -87,9 +109,9 @@ func TestRepository_FilterBlocks(t *testing.T) { AfterSeqNo: &nextSeqNo, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, []*core.Block{master}, res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, []*core.Block{master}, res.Rows) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index b6ed7c94..8e38afd8 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -164,6 +164,7 @@ func TestRelations(t *testing.T) { transaction.Workchain = shard.Workchain transaction.Shard = shard.Shard transaction.BlockSeqNo = shard.SeqNo + shard.TransactionsCount++ shard.Transactions = []*core.Transaction{transaction} shard.MasterID = &core.BlockID{Workchain: master.Workchain, Shard: master.Shard, SeqNo: master.SeqNo} diff --git a/internal/core/rndm/block.go b/internal/core/rndm/block.go index c206f98b..a85368ed 100644 --- a/internal/core/rndm/block.go +++ b/internal/core/rndm/block.go @@ -10,8 +10,8 @@ var ( seqNo uint32 = 100000 ) -func BlockID(workchain int32) *core.BlockID { - return &core.BlockID{ +func BlockID(workchain int32) core.BlockID { + return core.BlockID{ Workchain: workchain, Shard: -9223372036854775808, SeqNo: seqNo, diff --git a/internal/core/rndm/tx.go b/internal/core/rndm/tx.go index 8c206a59..cdd1b81a 100644 --- a/internal/core/rndm/tx.go +++ b/internal/core/rndm/tx.go @@ -13,7 +13,7 @@ var ( txLT uint64 = 80000 ) -func BlockTransaction(b *core.BlockID) *core.Transaction { +func BlockTransaction(b core.BlockID) *core.Transaction { txTS = txTS.Add(time.Minute) txLT++ @@ -39,7 +39,7 @@ func BlockTransaction(b *core.BlockID) *core.Transaction { } } -func BlockTransactions(b *core.BlockID, n int) (ret []*core.Transaction) { +func BlockTransactions(b core.BlockID, n int) (ret []*core.Transaction) { for i := 0; i < n; i++ { ret = append(ret, BlockTransaction(b)) } @@ -60,7 +60,7 @@ func AddressTransactions(a *addr.Address, n int) (ret []*core.Transaction) { } func Transaction() *core.Transaction { - return BlockTransaction(&core.BlockID{ + return BlockTransaction(core.BlockID{ Workchain: 0, Shard: int64(rand.Uint64()), SeqNo: rand.Uint32(), From d10a475a14de934188949cff0d37c85d6ec60b30 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 10 Oct 2023 22:06:04 +0530 Subject: [PATCH 005/186] [test] replace assert with require --- abi/known/telemint_test.go | 10 +- abi/known/tep62_nft_test.go | 16 +-- abi/known/tep74_test.go | 10 +- abi/known/tep81_dns_test.go | 3 +- abi/known/tonpay_test.go | 10 +- abi/known/wallets_test.go | 4 +- addr/address_test.go | 31 ++-- internal/api/http/server_test.go | 8 +- .../core/repository/account/account_test.go | 47 +++--- .../core/repository/account/aggregate_test.go | 39 ++--- .../core/repository/account/filter_test.go | 82 +++++------ .../core/repository/account/history_test.go | 16 +-- .../core/repository/msg/aggregate_test.go | 39 ++--- internal/core/repository/msg/filter_test.go | 44 +++--- internal/core/repository/msg/history_test.go | 21 +-- internal/core/repository/msg/msg_test.go | 28 ++-- internal/core/repository/repository_test.go | 134 +++++++++--------- internal/core/repository/tx/filter_test.go | 38 ++--- internal/core/repository/tx/history_test.go | 28 ++-- internal/core/repository/tx/tx_test.go | 31 ++-- 20 files changed, 321 insertions(+), 318 deletions(-) diff --git a/abi/known/telemint_test.go b/abi/known/telemint_test.go index 8f1035c2..41bd56b5 100644 --- a/abi/known/telemint_test.go +++ b/abi/known/telemint_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" @@ -93,7 +93,7 @@ func TestNewOperationDesc_TelemintNFTCollection(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -142,7 +142,7 @@ func TestNewOperationDesc_TelemintNFTItem(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -191,7 +191,7 @@ func TestOperationDesc_TelemintNFTCollection(t *testing.T) { for _, test := range testCases { j := loadOperation(t, i, test.name, test.boc) - assert.Equal(t, test.expected, j) + require.Equal(t, test.expected, j) } } @@ -253,6 +253,6 @@ func TestGetMethodDesc_TelemintNFTItem(t *testing.T) { for _, test := range testCases { ret := execGetMethod(t, i, test.addr, test.name, test.code, test.data) - assert.Equal(t, test.expected, ret) + require.Equal(t, test.expected, ret) } } diff --git a/abi/known/tep62_nft_test.go b/abi/known/tep62_nft_test.go index 843c8bbf..3eecfc5e 100644 --- a/abi/known/tep62_nft_test.go +++ b/abi/known/tep62_nft_test.go @@ -5,14 +5,14 @@ import ( "math/big" "testing" - "github.com/goccy/go-json" "github.com/stretchr/testify/require" + + "github.com/goccy/go-json" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" - "github.com/stretchr/testify/assert" - "github.com/tonindexer/anton/abi" ) @@ -70,7 +70,7 @@ func TestNewOperationDesc_NFTCollection(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -123,7 +123,7 @@ func TestLoadOperation_NFTCollection(t *testing.T) { j, err := json.Marshal(op) require.Nil(t, err) - assert.Equal(t, test.expected, string(j)) + require.Equal(t, test.expected, string(j)) } } @@ -159,7 +159,7 @@ func TestNewOperationDesc_NFTRoyalty(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -223,7 +223,7 @@ func TestNewOperationDesc_NFTItem(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -272,6 +272,6 @@ func TestNewOperationDesc_NFTEditable(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } diff --git a/abi/known/tep74_test.go b/abi/known/tep74_test.go index a56ff9b4..a03d3927 100644 --- a/abi/known/tep74_test.go +++ b/abi/known/tep74_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" @@ -72,7 +72,7 @@ func TestNewOperationDesc_JettonMinter(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -137,7 +137,7 @@ func TestNewOperationDesc_JettonWallet(t *testing.T) { require.Nil(t, err) got, err := json.Marshal(d) require.Nil(t, err) - assert.Equal(t, test.expected, string(got)) + require.Equal(t, test.expected, string(got)) } } @@ -176,7 +176,7 @@ func TestOperationDesc_JettonMinter(t *testing.T) { for _, test := range testCases { j := loadOperation(t, i, test.name, test.boc) - assert.Equal(t, test.expected, j) + require.Equal(t, test.expected, j) } } @@ -242,6 +242,6 @@ func TestOperationDesc_JettonWallet_JettonBurn_Optional(t *testing.T) { j, err := json.Marshal(op) require.Nil(t, err) - assert.Equal(t, test.expected, string(j)) + require.Equal(t, test.expected, string(j)) } } diff --git a/abi/known/tep81_dns_test.go b/abi/known/tep81_dns_test.go index ace9a6af..286f7a9f 100644 --- a/abi/known/tep81_dns_test.go +++ b/abi/known/tep81_dns_test.go @@ -6,7 +6,6 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xssnick/tonutils-go/address" @@ -63,6 +62,6 @@ func TestGetMethodDesc_DNSItem(t *testing.T) { for _, test := range testCases { ret := execGetMethod(t, i, test.addr, test.name, test.code, test.data) - assert.Equal(t, test.expected, ret) + require.Equal(t, test.expected, ret) } } diff --git a/abi/known/tonpay_test.go b/abi/known/tonpay_test.go index 10820bf7..22b037fd 100644 --- a/abi/known/tonpay_test.go +++ b/abi/known/tonpay_test.go @@ -6,8 +6,8 @@ import ( "reflect" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" @@ -77,7 +77,7 @@ func TestOperationDesc_TonpayStore(t *testing.T) { for _, test := range testCases { j := loadOperation(t, i, test.name, test.boc) - assert.Equal(t, test.expected, j) + require.Equal(t, test.expected, j) } } @@ -158,7 +158,7 @@ func TestGetMethodDesc_TonpayStore(t *testing.T) { if reflect.TypeOf(test.expected[it]) == reflect.TypeOf(&cell.Cell{}) { continue } - assert.Equal(t, test.expected[it], ret[it]) + require.Equal(t, test.expected[it], ret[it]) } } } @@ -198,7 +198,7 @@ func TestOperationDesc_TonpayInvoice(t *testing.T) { for _, test := range testCases { j := loadOperation(t, i, test.name, test.boc) - assert.Equal(t, test.expected, j) + require.Equal(t, test.expected, j) } } @@ -259,7 +259,7 @@ func TestGetMethodDesc_TonpayInvoice(t *testing.T) { if reflect.TypeOf(test.expected[it]) == reflect.TypeOf(&cell.Cell{}) { continue } - assert.Equal(t, test.expected[it], ret[it]) + require.Equal(t, test.expected[it], ret[it]) } } } diff --git a/abi/known/wallets_test.go b/abi/known/wallets_test.go index 7ca04259..6684d532 100644 --- a/abi/known/wallets_test.go +++ b/abi/known/wallets_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/address" "github.com/tonindexer/anton/abi" @@ -75,6 +75,6 @@ func TestGetMethodDesc_Wallets(t *testing.T) { for _, test := range testCases { ret := execGetMethod(t, i, test.addr, test.name, test.code, test.data) - assert.Equal(t, test.expected, ret) + require.Equal(t, test.expected, ret) } } diff --git a/addr/address_test.go b/addr/address_test.go index ea677656..bdf1d6df 100644 --- a/addr/address_test.go +++ b/addr/address_test.go @@ -5,28 +5,27 @@ import ( "reflect" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAddress_TypeKind(t *testing.T) { a, err := new(Address).FromBase64("Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF") - assert.Nil(t, err) - assert.Equal(t, int8(-1), a.Workchain()) + require.Nil(t, err) + require.Equal(t, int8(-1), a.Workchain()) v := reflect.ValueOf(a) vt := v.Type() - assert.Equal(t, reflect.Pointer, vt.Kind()) - assert.Equal(t, reflect.Array, vt.Elem().Kind()) - assert.Equal(t, reflect.Uint8, vt.Elem().Elem().Kind()) - assert.True(t, vt.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem())) + require.Equal(t, reflect.Pointer, vt.Kind()) + require.Equal(t, reflect.Array, vt.Elem().Kind()) + 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() - assert.Nil(t, err) + require.Nil(t, err) rb, ok := r.([]byte) - assert.True(t, ok) + require.True(t, ok) t.Logf("%+v\n", rb) } @@ -50,20 +49,20 @@ func TestAddress_FromBase64(t *testing.T) { for _, c := range testCases { addr, err := new(Address).FromBase64(c.b64) - assert.Nil(t, err) + require.Nil(t, err) addrStr := addr.String() - assert.Equal(t, c.uf, addrStr) - assert.Equal(t, c.b64, addr.Base64()) + require.Equal(t, c.uf, addrStr) + require.Equal(t, c.b64, addr.Base64()) addrGot, err := new(Address).FromString(addrStr) - assert.Nil(t, err) - assert.Equal(t, c.b64, addrGot.Base64()) + require.Nil(t, err) + require.Equal(t, c.b64, addrGot.Base64()) addrTU, err := addrGot.ToTonutils() - assert.Nil(t, err) + require.Nil(t, err) addrFromTU := MustFromTonutils(addrTU) - assert.Equal(t, c.b64, addrFromTU.Base64()) + require.Equal(t, c.b64, addrFromTU.Base64()) } } diff --git a/internal/api/http/server_test.go b/internal/api/http/server_test.go index c5391569..cf825804 100644 --- a/internal/api/http/server_test.go +++ b/internal/api/http/server_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/app/query" @@ -23,10 +23,10 @@ func testService(t *testing.T) *query.Service { bd, err := repository.ConnectDB(ctx, "clickhouse://user:pass@localhost:9000/postgres?sslmode=disable", "postgres://user:pass@localhost:5432/postgres?sslmode=disable") - assert.Nil(t, err) + require.Nil(t, err) s, err := query.NewService(context.Background(), &app.QueryConfig{DB: bd}) - assert.Nil(t, err) + require.Nil(t, err) _testService = s return _testService @@ -39,5 +39,5 @@ func TestServer_Start(t *testing.T) { s.RegisterRoutes(c) - assert.Nil(t, s.Run()) + require.Nil(t, s.Run()) } diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index d17724d3..da8d87e7 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -6,7 +6,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" @@ -34,18 +35,18 @@ func initdb(t testing.TB) { ck = ch.Connect(ch.WithDSN(dsnCH), ch.WithAutoCreateDatabase(true), ch.WithPoolSize(16)) err = ck.Ping(ctx) - assert.Nil(t, err) + require.Nil(t, err) pg = bun.NewDB(sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))), pgdialect.New()) err = pg.Ping() - assert.Nil(t, err) + require.Nil(t, err) repo = account.NewRepository(ck, pg) } func createTables(t testing.TB) { err := account.CreateTables(context.Background(), ck, pg) - assert.Nil(t, err) + require.Nil(t, err) } func dropTables(t testing.TB) { @@ -53,20 +54,20 @@ func dropTables(t testing.TB) { defer cancel() _, err := pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.AccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AddressLabel)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.AddressLabel)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS account_status") - assert.Nil(t, err) + require.Nil(t, err) } func TestRepository_AddAccounts(t *testing.T) { @@ -79,7 +80,7 @@ func TestRepository_AddAccounts(t *testing.T) { defer cancel() tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) t.Run("drop tables", func(t *testing.T) { dropTables(t) @@ -91,23 +92,23 @@ func TestRepository_AddAccounts(t *testing.T) { t.Run("add account states", func(t *testing.T) { err := repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) got := new(core.AccountState) err = tx.NewSelect().Model(got).Where("address = ?", a).Where("last_tx_lt = ?", states[0].LastTxLT).Scan(ctx) - assert.Nil(t, err) - assert.Equal(t, states[0], got) + require.Nil(t, err) + require.Equal(t, states[0], got) err = ck.NewSelect().Model(got).Where("address = ?", a).Where("last_tx_lt = ?", states[0].LastTxLT).Scan(ctx) - assert.Nil(t, err) + require.Nil(t, err) got.UpdatedAt = states[0].UpdatedAt // TODO: look at time.Time ch unmarshal - assert.Equal(t, states[0], got) + require.Equal(t, states[0], got) }) t.Run("commit transaction", func(t *testing.T) { err := tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("drop tables again", func(t *testing.T) { @@ -131,7 +132,7 @@ func BenchmarkRepository_AddAccounts(b *testing.B) { b.Run("insert many addresses", func(b *testing.B) { for i := 0; i < b.N; i++ { tx, err := pg.Begin() - assert.Nil(b, err) + require.Nil(b, err) states := rndm.AccountStates(30) states = append(states, rndm.AccountStates(30)...) @@ -139,10 +140,10 @@ func BenchmarkRepository_AddAccounts(b *testing.B) { states = append(states, rndm.AccountStatesContract(30, "", nil)...) err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(b, err) + require.Nil(b, err) err = tx.Commit() - assert.Nil(b, err) + require.Nil(b, err) } }) @@ -151,15 +152,15 @@ func BenchmarkRepository_AddAccounts(b *testing.B) { for i := 0; i < b.N; i++ { tx, err := pg.Begin() - assert.Nil(b, err) + require.Nil(b, err) states := rndm.AddressStates(a, 1) err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(b, err) + require.Nil(b, err) err = tx.Commit() - assert.Nil(b, err) + require.Nil(b, err) } }) diff --git a/internal/core/repository/account/aggregate_test.go b/internal/core/repository/account/aggregate_test.go index 0c2d1346..8263c55c 100644 --- a/internal/core/repository/account/aggregate_test.go +++ b/internal/core/repository/account/aggregate_test.go @@ -5,7 +5,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun/extra/bunbig" "github.com/tonindexer/anton/abi/known" @@ -37,7 +38,7 @@ func TestRepository_AggregateAccounts_NFTCollection(t *testing.T) { t.Run("insert test collection data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) collectionStates = rndm.AccountStatesContract(100, known.NFTCollection, nil) @@ -47,10 +48,10 @@ func TestRepository_AggregateAccounts_NFTCollection(t *testing.T) { } err = repo.AddAccountStates(ctx, tx, append(itemsStates, collectionStates...)) - assert.Nil(t, err) + require.Nil(t, err) err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("aggregate collections info", func(t *testing.T) { @@ -58,16 +59,16 @@ func TestRepository_AggregateAccounts_NFTCollection(t *testing.T) { MinterAddress: &collectionStates[0].Address, Limit: 25, }) - assert.Nil(t, err) - assert.Equal(t, itemCount, res.Items) - assert.Equal(t, itemCount, res.OwnersCount) - assert.Equal(t, itemCount, len(res.OwnedItems)) + require.Nil(t, err) + require.Equal(t, itemCount, res.Items) + require.Equal(t, itemCount, res.OwnersCount) + require.Equal(t, itemCount, len(res.OwnedItems)) for _, c := range res.OwnedItems { - assert.Equal(t, 1, c.ItemsCount) + require.Equal(t, 1, c.ItemsCount) } - assert.Equal(t, itemCount, len(res.UniqueOwners)) + require.Equal(t, itemCount, len(res.UniqueOwners)) for _, c := range res.UniqueOwners { - assert.Equal(t, 100/itemCount, c.OwnersCount) + require.Equal(t, 100/itemCount, c.OwnersCount) } }) @@ -102,7 +103,7 @@ func TestRepository_AggregateAccounts_JettonMinter(t *testing.T) { t.Run("insert test jetton data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) minterStates = rndm.AccountStatesContract(100, known.JettonMinter, nil) @@ -116,10 +117,10 @@ func TestRepository_AggregateAccounts_JettonMinter(t *testing.T) { } err = repo.AddAccountStates(ctx, tx, append(walletsStates, minterStates...)) - assert.Nil(t, err) + require.Nil(t, err) err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("aggregate jetton data", func(t *testing.T) { @@ -127,12 +128,12 @@ func TestRepository_AggregateAccounts_JettonMinter(t *testing.T) { MinterAddress: &minterStates[0].Address, Limit: 25, }) - assert.Nil(t, err) - assert.Equal(t, walletsCount, res.Wallets) - assert.Equal(t, totalSupply, res.TotalSupply) - assert.Equal(t, walletsCount, len(res.OwnedBalance)) + require.Nil(t, err) + require.Equal(t, walletsCount, res.Wallets) + require.Equal(t, totalSupply, res.TotalSupply) + require.Equal(t, walletsCount, len(res.OwnedBalance)) for _, b := range res.OwnedBalance { - assert.Equal(t, ownedBalance[*b.OwnerAddress], b.Balance) + require.Equal(t, ownedBalance[*b.OwnerAddress], b.Balance) } }) diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index 1f65bde9..f9b33c86 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/abi" @@ -121,7 +121,7 @@ func TestRepository_FilterAccounts(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) for i := 0; i < 10; i++ { // 10 account states on 100 addresses var states []*core.AccountState @@ -135,16 +135,16 @@ func TestRepository_FilterAccounts(t *testing.T) { addressStates = states[len(states)-10:] err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("insert states with special contract type", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) // filter by contract interfaces for i := 0; i < 15; i++ { // add 15 addresses with 10 states @@ -153,25 +153,25 @@ func TestRepository_FilterAccounts(t *testing.T) { specialState = states[len(states)-1] err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("insert many states on some address", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) for i := 0; i < 5; i++ { latestState = rndm.AddressStateContract(address, "", nil) err = repo.AddAccountStates(ctx, tx, []*core.AccountState{latestState}) - assert.Nil(t, err) + require.Nil(t, err) } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("filter states by address", func(t *testing.T) { @@ -179,9 +179,9 @@ func TestRepository_FilterAccounts(t *testing.T) { Addresses: []*addr.Address{address}, Order: "ASC", Limit: len(addressStates), }) - assert.Nil(t, err) - assert.Equal(t, 15, results.Total) - assert.Equal(t, addressStates, results.Rows) + require.Nil(t, err) + require.Equal(t, 15, results.Total) + require.Equal(t, addressStates, results.Rows) }) t.Run("filter latest state by address and exclude columns", func(t *testing.T) { @@ -193,9 +193,9 @@ func TestRepository_FilterAccounts(t *testing.T) { LatestState: true, ExcludeColumn: []string{"code"}, }) - assert.Nil(t, err) - assert.Equal(t, 1, results.Total) - assert.Equal(t, []*core.AccountState{&latest}, results.Rows) + require.Nil(t, err) + require.Equal(t, 1, results.Total) + require.Equal(t, []*core.AccountState{&latest}, results.Rows) }) t.Run("filter latest state with data by address and exclude columns", func(t *testing.T) { @@ -207,9 +207,9 @@ func TestRepository_FilterAccounts(t *testing.T) { LatestState: true, ExcludeColumn: []string{"code"}, }) - assert.Nil(t, err) - assert.Equal(t, 1, results.Total) - assert.Equal(t, []*core.AccountState{&latest}, results.Rows) + require.Nil(t, err) + require.Equal(t, 1, results.Total) + require.Equal(t, []*core.AccountState{&latest}, results.Rows) }) t.Run("filter latest state with data by contract types", func(t *testing.T) { @@ -218,9 +218,9 @@ func TestRepository_FilterAccounts(t *testing.T) { LatestState: true, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 15, results.Total) - assert.Equal(t, []*core.AccountState{specialState}, results.Rows) + require.Nil(t, err) + require.Equal(t, 15, results.Total) + require.Equal(t, []*core.AccountState{specialState}, results.Rows) }) t.Run("filter states by minter", func(t *testing.T) { @@ -228,9 +228,9 @@ func TestRepository_FilterAccounts(t *testing.T) { MinterAddress: latestState.MinterAddress, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 5, results.Total) - assert.Equal(t, []*core.AccountState{latestState}, results.Rows) + require.Nil(t, err) + require.Equal(t, 5, results.Total) + require.Equal(t, []*core.AccountState{latestState}, results.Rows) }) t.Run("filter states by owner", func(t *testing.T) { @@ -238,9 +238,9 @@ func TestRepository_FilterAccounts(t *testing.T) { OwnerAddress: latestState.OwnerAddress, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 1, results.Total) - assert.Equal(t, []*core.AccountState{latestState}, results.Rows) + require.Nil(t, err) + require.Equal(t, 1, results.Total) + require.Equal(t, []*core.AccountState{latestState}, results.Rows) }) t.Run("filter latest states by owner", func(t *testing.T) { @@ -249,9 +249,9 @@ func TestRepository_FilterAccounts(t *testing.T) { OwnerAddress: latestState.OwnerAddress, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 1, results.Total) - assert.Equal(t, []*core.AccountState{latestState}, results.Rows) + require.Nil(t, err) + require.Equal(t, 1, results.Total) + require.Equal(t, []*core.AccountState{latestState}, results.Rows) }) t.Run("drop tables again", func(t *testing.T) { @@ -288,13 +288,13 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) for i := 0; i < totalStates/100; i++ { states := rndm.AccountStates(100) err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) if i%100 == 0 { t.Logf("%s: add %d states on %s address", time.Now().UTC(), 100*(i+1), states[0].Address.String()) @@ -302,12 +302,12 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("insert states with special contract type", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) address = rndm.Address() @@ -315,7 +315,7 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { specialState = rndm.AddressStateContract(address, "special", nil) err = repo.AddAccountStates(ctx, tx, []*core.AccountState{specialState}) - assert.Nil(t, err) + require.Nil(t, err) if i%100 == 0 { t.Logf("%s: add %d special states", time.Now().UTC(), 100*(i+1)) @@ -323,7 +323,7 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("filter latest state with data by contract types", func(t *testing.T) { @@ -334,10 +334,10 @@ func TestRepository_FilterAccounts_Heavy(t *testing.T) { LatestState: true, Order: "DESC", Limit: 1, }) - assert.Nil(t, err) - assert.Equal(t, 1, results.Total) - assert.Equal(t, []*core.AccountState{specialState}, results.Rows) - assert.Less(t, time.Since(start), 2*time.Second) + require.Nil(t, err) + require.Equal(t, 1, results.Total) + require.Equal(t, []*core.AccountState{specialState}, results.Rows) + require.Less(t, time.Since(start), 2*time.Second) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/account/history_test.go b/internal/core/repository/account/history_test.go index 51edbf0d..f236c240 100644 --- a/internal/core/repository/account/history_test.go +++ b/internal/core/repository/account/history_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core/aggregate/history" @@ -28,24 +28,24 @@ func TestRepository_AggregateAccountsHistory(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) for i := 0; i < 10; i++ { states := rndm.AccountStates(10) err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) } for i := 0; i < 10; i++ { states := rndm.AccountStatesContract(10, "special", nil) err = repo.AddAccountStates(ctx, tx, states) - assert.Nil(t, err) + require.Nil(t, err) } err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("count active addresses", func(t *testing.T) { @@ -57,10 +57,10 @@ func TestRepository_AggregateAccountsHistory(t *testing.T) { Interval: 24 * time.Hour, }, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.CountRes)) + require.Nil(t, err) + require.Equal(t, 1, len(res.CountRes)) for _, c := range res.CountRes { - assert.Equal(t, 10, c.Value) + require.Equal(t, 10, c.Value) } }) diff --git a/internal/core/repository/msg/aggregate_test.go b/internal/core/repository/msg/aggregate_test.go index 866e8959..464af38f 100644 --- a/internal/core/repository/msg/aggregate_test.go +++ b/internal/core/repository/msg/aggregate_test.go @@ -5,7 +5,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun/extra/bunbig" "github.com/tonindexer/anton/addr" @@ -49,17 +50,17 @@ func TestRepository_AggregateMessages(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddMessages(ctx, tx, rndm.Messages(100)) - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddMessages(ctx, tx, messagesTo) - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddMessages(ctx, tx, messagesFrom) - assert.Nil(t, err) + require.Nil(t, err) err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("aggregate by address", func(t *testing.T) { @@ -68,22 +69,22 @@ func TestRepository_AggregateMessages(t *testing.T) { OrderBy: "amount", Limit: 150, }) - assert.Nil(t, err) - assert.Equal(t, recvCount, res.RecvCount) - assert.Equal(t, recvAmount, res.RecvAmount) + require.Nil(t, err) + require.Equal(t, recvCount, res.RecvCount) + require.Equal(t, recvAmount, res.RecvAmount) for _, r := range res.RecvByAddress { - assert.True(t, r.Sender != nil) - assert.True(t, r.Amount != nil && r.Amount.ToUInt64() > 0) - assert.Equal(t, recvFromAddress[*r.Sender], r.Amount) - assert.Equal(t, 1, r.Count) + require.True(t, r.Sender != nil) + require.True(t, r.Amount != nil && r.Amount.ToUInt64() > 0) + require.Equal(t, recvFromAddress[*r.Sender], r.Amount) + require.Equal(t, 1, r.Count) } - assert.Equal(t, sentCount, res.SentCount) - assert.Equal(t, sentAmount, res.SentAmount) + require.Equal(t, sentCount, res.SentCount) + require.Equal(t, sentAmount, res.SentAmount) for _, r := range res.SentByAddress { - assert.True(t, r.Receiver != nil) - assert.True(t, r.Amount != nil && r.Amount.ToUInt64() > 0) - assert.Equal(t, sentToAddress[*r.Receiver], r.Amount) - assert.Equal(t, 1, r.Count) + require.True(t, r.Receiver != nil) + require.True(t, r.Amount != nil && r.Amount.ToUInt64() > 0) + require.Equal(t, sentToAddress[*r.Receiver], r.Amount) + require.Equal(t, 1, r.Count) } }) } diff --git a/internal/core/repository/msg/filter_test.go b/internal/core/repository/msg/filter_test.go index 15beaaea..61244225 100644 --- a/internal/core/repository/msg/filter_test.go +++ b/internal/core/repository/msg/filter_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" @@ -37,13 +37,13 @@ func TestRepository_FilterMessages(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddMessages(ctx, tx, messages) - assert.Nil(t, err) + require.Nil(t, err) err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("filter by hash", func(t *testing.T) { @@ -52,44 +52,44 @@ func TestRepository_FilterMessages(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ Hash: messages[0].Hash, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, 1, len(res.Rows)) - assert.JSONEq(t, string(expected.DataJSON), string(res.Rows[0].DataJSON)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, 1, len(res.Rows)) + require.JSONEq(t, string(expected.DataJSON), string(res.Rows[0].DataJSON)) res.Rows[0].DataJSON = expected.DataJSON - assert.Equal(t, []*core.Message{&expected}, res.Rows) + require.Equal(t, []*core.Message{&expected}, res.Rows) }) t.Run("filter by address", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ DstAddresses: []*addr.Address{&messages[0].DstAddress}, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, []*core.Message{messages[0]}, res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, []*core.Message{messages[0]}, res.Rows) }) t.Run("filter by contract", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ DstContracts: []string{"special"}, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, 1, len(res.Rows)) - assert.JSONEq(t, string(specialDestination.DataJSON), string(res.Rows[0].DataJSON)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, 1, len(res.Rows)) + require.JSONEq(t, string(specialDestination.DataJSON), string(res.Rows[0].DataJSON)) res.Rows[0].DataJSON = specialDestination.DataJSON - assert.Equal(t, []*core.Message{specialDestination}, res.Rows) + require.Equal(t, []*core.Message{specialDestination}, res.Rows) }) t.Run("filter by operation name", func(t *testing.T) { res, err := repo.FilterMessages(ctx, &filter.MessagesReq{ OperationNames: []string{"special_op"}, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, 1, len(res.Rows)) - assert.JSONEq(t, string(specialOperation.DataJSON), string(res.Rows[0].DataJSON)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, 1, len(res.Rows)) + require.JSONEq(t, string(specialOperation.DataJSON), string(res.Rows[0].DataJSON)) res.Rows[0].DataJSON = specialOperation.DataJSON - assert.Equal(t, []*core.Message{specialOperation}, res.Rows) + require.Equal(t, []*core.Message{specialOperation}, res.Rows) }) } diff --git a/internal/core/repository/msg/history_test.go b/internal/core/repository/msg/history_test.go index 86d718a4..33e05f22 100644 --- a/internal/core/repository/msg/history_test.go +++ b/internal/core/repository/msg/history_test.go @@ -5,7 +5,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun/extra/bunbig" "github.com/tonindexer/anton/internal/core/aggregate/history" @@ -32,7 +33,7 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { t.Run("insert test data", func(t *testing.T) { tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) messages := rndm.Messages(50) for _, m := range messages { @@ -44,10 +45,10 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { } err = repo.AddMessages(ctx, tx, messages) - assert.Nil(t, err) + require.Nil(t, err) err = tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("count messages to special contract", func(t *testing.T) { @@ -59,9 +60,9 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { Interval: 24 * time.Hour, }, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.CountRes)) - assert.Equal(t, 50, res.CountRes[0].Value) + require.Nil(t, err) + require.Equal(t, 1, len(res.CountRes)) + require.Equal(t, 50, res.CountRes[0].Value) }) t.Run("sum messages amount to special contract", func(t *testing.T) { @@ -73,9 +74,9 @@ func TestRepository_AggregateMessagesHistory(t *testing.T) { Interval: 24 * time.Hour, }, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.BigIntRes)) - assert.Equal(t, amountSum, res.BigIntRes[0].Value) + require.Nil(t, err) + require.Equal(t, 1, len(res.BigIntRes)) + require.Equal(t, amountSum, res.BigIntRes[0].Value) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/msg/msg_test.go b/internal/core/repository/msg/msg_test.go index eeb44c3b..07a4c9f0 100644 --- a/internal/core/repository/msg/msg_test.go +++ b/internal/core/repository/msg/msg_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" @@ -36,18 +36,18 @@ func initdb(t testing.TB) { ck = ch.Connect(ch.WithDSN(dsnCH), ch.WithAutoCreateDatabase(true), ch.WithPoolSize(16)) err = ck.Ping(ctx) - assert.Nil(t, err) + require.Nil(t, err) pg = bun.NewDB(sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))), pgdialect.New()) err = pg.Ping() - assert.Nil(t, err) + require.Nil(t, err) repo = msg.NewRepository(ck, pg) } func createTables(t testing.TB) { err := msg.CreateTables(context.Background(), ck, pg) - assert.Nil(t, err) + require.Nil(t, err) } func dropTables(t testing.TB) { @@ -55,12 +55,12 @@ func dropTables(t testing.TB) { defer cancel() _, err := ck.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS message_type") - assert.Nil(t, err) + require.Nil(t, err) } func TestRepository_AddMessages(t *testing.T) { @@ -72,7 +72,7 @@ func TestRepository_AddMessages(t *testing.T) { defer cancel() tx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) t.Run("drop tables", func(t *testing.T) { dropTables(t) @@ -84,22 +84,22 @@ func TestRepository_AddMessages(t *testing.T) { t.Run("add messages", func(t *testing.T) { err := repo.AddMessages(ctx, tx, messages) - assert.Nil(t, err) + require.Nil(t, err) got := new(core.Message) err = tx.NewSelect().Model(got).Where("hash = ?", messages[0].Hash).Scan(ctx) - assert.Nil(t, err) - assert.Equal(t, messages[0], got) + require.Nil(t, err) + require.Equal(t, messages[0], got) err = ck.NewSelect().Model(got).Where("hash = ?", messages[0].Hash).Scan(ctx) - assert.Nil(t, err) + require.Nil(t, err) got.CreatedAt = messages[0].CreatedAt // TODO: look at time.Time ch unmarshal - assert.Equal(t, messages[0], got) + require.Equal(t, messages[0], got) }) t.Run("commit transaction", func(t *testing.T) { err := tx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) } diff --git a/internal/core/repository/repository_test.go b/internal/core/repository/repository_test.go index 8e38afd8..44d5582b 100644 --- a/internal/core/repository/repository_test.go +++ b/internal/core/repository/repository_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" @@ -55,43 +55,43 @@ func dropTables(t testing.TB) { ck, pg := db.CH, db.PG _, err := ck.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Message)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.LatestAccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.AccountState)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.AddressLabel)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.AddressLabel)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS account_status") - assert.Nil(t, err) + require.Nil(t, err) _, err = ck.NewDropTable().Model((*core.Block)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Block)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractInterface)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS message_type") - assert.Nil(t, err) + require.Nil(t, err) } func createTables(t testing.TB) { @@ -99,19 +99,19 @@ func createTables(t testing.TB) { defer cancel() err := block.CreateTables(ctx, db.CH, db.PG) - assert.Nil(t, err) + require.Nil(t, err) err = account.CreateTables(ctx, db.CH, db.PG) - assert.Nil(t, err) + require.Nil(t, err) err = tx.CreateTables(ctx, db.CH, db.PG) - assert.Nil(t, err) + require.Nil(t, err) err = msg.CreateTables(ctx, db.CH, db.PG) - assert.Nil(t, err) + require.Nil(t, err) err = contract.CreateTables(ctx, db.PG) - assert.Nil(t, err) + require.Nil(t, err) } func TestInsertKnownInterfaces(t *testing.T) { @@ -188,19 +188,19 @@ func TestRelations(t *testing.T) { t.Run("insert related data", func(t *testing.T) { dbtx, err := db.PG.Begin() - assert.Nil(t, err) + require.Nil(t, err) err = accountRepo.AddAccountStates(ctx, dbtx, states) - assert.Nil(t, err) + require.Nil(t, err) err = msgRepo.AddMessages(ctx, dbtx, messages) - assert.Nil(t, err) + require.Nil(t, err) err = txRepo.AddTransactions(ctx, dbtx, transactions) - assert.Nil(t, err) + require.Nil(t, err) err = blockRepo.AddBlocks(ctx, dbtx, blocks) - assert.Nil(t, err) + require.Nil(t, err) err = dbtx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("get account states with data", func(t *testing.T) { @@ -208,23 +208,23 @@ func TestRelations(t *testing.T) { Addresses: addresses, LatestState: true, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, states, res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, states, res.Rows) }) t.Run("get messages with payloads", func(t *testing.T) { res, err := msgRepo.FilterMessages(ctx, &filter.MessagesReq{ DstAddresses: addresses, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, len(messagesTo), len(res.Rows)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, len(messagesTo), len(res.Rows)) for i := range messagesTo { - assert.JSONEq(t, string(messagesTo[i].DataJSON), string(res.Rows[i].DataJSON)) + require.JSONEq(t, string(messagesTo[i].DataJSON), string(res.Rows[i].DataJSON)) res.Rows[i].DataJSON = messagesTo[i].DataJSON } - assert.Equal(t, messagesTo, res.Rows) + require.Equal(t, messagesTo, res.Rows) }) t.Run("get transactions with states and messages", func(t *testing.T) { @@ -233,13 +233,13 @@ func TestRelations(t *testing.T) { WithAccountState: true, WithMessages: true, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, 1, len(res.Rows)) - assert.NotNil(t, res.Rows[0].InMsg) - assert.JSONEq(t, string(messagesTo[0].DataJSON), string(res.Rows[0].InMsg.DataJSON)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, 1, len(res.Rows)) + require.NotNil(t, res.Rows[0].InMsg) + require.JSONEq(t, string(messagesTo[0].DataJSON), string(res.Rows[0].InMsg.DataJSON)) res.Rows[0].InMsg.DataJSON = messagesTo[0].DataJSON - assert.Equal(t, transactions, res.Rows) + require.Equal(t, transactions, res.Rows) }) t.Run("get master block with shards and transactions", func(t *testing.T) { @@ -251,37 +251,37 @@ func TestRelations(t *testing.T) { WithTransactionAccountState: true, WithTransactionMessages: true, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, 1, len(res.Rows)) - assert.Equal(t, 1, len(res.Rows[0].Shards)) - assert.Equal(t, 1, len(res.Rows[0].Shards[0].Transactions)) - assert.NotNil(t, res.Rows[0].Shards[0].Transactions[0].InMsg) - assert.JSONEq(t, string(messagesTo[0].DataJSON), string(res.Rows[0].Shards[0].Transactions[0].InMsg.DataJSON)) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, 1, len(res.Rows)) + require.Equal(t, 1, len(res.Rows[0].Shards)) + require.Equal(t, 1, len(res.Rows[0].Shards[0].Transactions)) + require.NotNil(t, res.Rows[0].Shards[0].Transactions[0].InMsg) + require.JSONEq(t, string(messagesTo[0].DataJSON), string(res.Rows[0].Shards[0].Transactions[0].InMsg.DataJSON)) res.Rows[0].Shards[0].Transactions[0].InMsg.DataJSON = messagesTo[0].DataJSON - assert.Equal(t, blocks[1:], res.Rows) + require.Equal(t, blocks[1:], res.Rows) }) t.Run("get statistics", func(t *testing.T) { stats, err := aggregate.GetStatistics(ctx, db.CH, db.PG) - assert.Nil(t, err) - assert.Equal(t, int(master.SeqNo), stats.FirstBlock) - assert.Equal(t, int(master.SeqNo), stats.LastBlock) - assert.Equal(t, 1, stats.MasterBlockCount) - assert.Equal(t, 1, stats.AddressCount) - assert.Equal(t, 1, stats.ParsedAddressCount) - assert.Equal(t, 1, stats.AccountCount) - assert.Equal(t, 1, stats.ParsedAccountCount) - assert.Equal(t, 1, stats.TransactionCount) - assert.Equal(t, 2, stats.MessageCount) - assert.Equal(t, 1, stats.ParsedMessageCount) - assert.Equal(t, 1, len(stats.AddressStatusCount)) - assert.Equal(t, core.Active, stats.AddressStatusCount[0].Status) - assert.Equal(t, 1, stats.AddressStatusCount[0].Count) - assert.Equal(t, state.Types, stats.AddressTypesCount[0].Interfaces) - assert.Equal(t, 1, stats.AddressTypesCount[0].Count) - assert.Equal(t, operation, stats.MessageTypesCount[0].Operation) - assert.Equal(t, 1, stats.MessageTypesCount[0].Count) + require.Nil(t, err) + require.Equal(t, int(master.SeqNo), stats.FirstBlock) + require.Equal(t, int(master.SeqNo), stats.LastBlock) + require.Equal(t, 1, stats.MasterBlockCount) + require.Equal(t, 1, stats.AddressCount) + require.Equal(t, 1, stats.ParsedAddressCount) + require.Equal(t, 1, stats.AccountCount) + require.Equal(t, 1, stats.ParsedAccountCount) + require.Equal(t, 1, stats.TransactionCount) + require.Equal(t, 2, stats.MessageCount) + require.Equal(t, 1, stats.ParsedMessageCount) + require.Equal(t, 1, len(stats.AddressStatusCount)) + require.Equal(t, core.Active, stats.AddressStatusCount[0].Status) + require.Equal(t, 1, stats.AddressStatusCount[0].Count) + require.Equal(t, state.Types, stats.AddressTypesCount[0].Interfaces) + require.Equal(t, 1, stats.AddressTypesCount[0].Count) + require.Equal(t, operation, stats.MessageTypesCount[0].Operation) + require.Equal(t, 1, stats.MessageTypesCount[0].Count) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/tx/filter_test.go b/internal/core/repository/tx/filter_test.go index 66f423cc..59d4134a 100644 --- a/internal/core/repository/tx/filter_test.go +++ b/internal/core/repository/tx/filter_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" @@ -31,40 +31,40 @@ func TestRepository_FilterTransactions(t *testing.T) { t.Run("add transactions", func(t *testing.T) { dbtx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddTransactions(ctx, dbtx, transactions) - assert.Nil(t, err) + require.Nil(t, err) err = dbtx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("filter by hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ Hash: transactions[0].Hash, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, transactions[0:1], res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, transactions[0:1], res.Rows) }) t.Run("filter by incoming message hash", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ InMsgHash: transactions[0].InMsgHash, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, transactions[0:1], res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, transactions[0:1], res.Rows) }) t.Run("filter by addresses", func(t *testing.T) { res, err := repo.FilterTransactions(ctx, &filter.TransactionsReq{ Addresses: []*addr.Address{&transactions[0].Address}, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, transactions[0:1], res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, transactions[0:1], res.Rows) }) t.Run("filter by block id", func(t *testing.T) { @@ -75,9 +75,9 @@ func TestRepository_FilterTransactions(t *testing.T) { SeqNo: transactions[0].BlockSeqNo, }, }) - assert.Nil(t, err) - assert.Equal(t, 1, res.Total) - assert.Equal(t, transactions[0:1], res.Rows) + require.Nil(t, err) + require.Equal(t, 1, res.Total) + require.Equal(t, transactions[0:1], res.Rows) }) t.Run("filter by workchain", func(t *testing.T) { @@ -86,9 +86,9 @@ func TestRepository_FilterTransactions(t *testing.T) { Order: "ASC", Limit: len(transactions), }) - assert.Nil(t, err) - assert.Equal(t, len(transactions), res.Total) - assert.Equal(t, transactions, res.Rows) + require.Nil(t, err) + require.Equal(t, len(transactions), res.Total) + require.Equal(t, transactions, res.Rows) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/tx/history_test.go b/internal/core/repository/tx/history_test.go index 7f59d9e2..ebe989de 100644 --- a/internal/core/repository/tx/history_test.go +++ b/internal/core/repository/tx/history_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core/aggregate/history" @@ -33,16 +33,16 @@ func TestRepository_AggregateTransactionsHistory(t *testing.T) { t.Run("add transactions", func(t *testing.T) { dbtx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddTransactions(ctx, dbtx, transactions) - assert.Nil(t, err) + require.Nil(t, err) err = repo.AddTransactions(ctx, dbtx, addrTransactions) - assert.Nil(t, err) + require.Nil(t, err) err = dbtx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("transaction count", func(t *testing.T) { @@ -50,9 +50,9 @@ func TestRepository_AggregateTransactionsHistory(t *testing.T) { Metric: history.TransactionCount, ReqParams: history.ReqParams{Interval: 4 * time.Hour}, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.CountRes)) - assert.Equal(t, 30, res.CountRes[0].Value) + require.Nil(t, err) + require.Equal(t, 1, len(res.CountRes)) + require.Equal(t, 30, res.CountRes[0].Value) }) t.Run("transaction count by workchain", func(t *testing.T) { @@ -61,9 +61,9 @@ func TestRepository_AggregateTransactionsHistory(t *testing.T) { Workchain: new(int32), ReqParams: history.ReqParams{Interval: 4 * time.Hour}, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.CountRes)) - assert.Equal(t, 20, res.CountRes[0].Value) + require.Nil(t, err) + require.Equal(t, 1, len(res.CountRes)) + require.Equal(t, 20, res.CountRes[0].Value) }) t.Run("transaction count by workchain", func(t *testing.T) { @@ -72,9 +72,9 @@ func TestRepository_AggregateTransactionsHistory(t *testing.T) { Addresses: []*addr.Address{a}, ReqParams: history.ReqParams{Interval: 4 * time.Hour}, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(res.CountRes)) - assert.Equal(t, 20, res.CountRes[0].Value) + require.Nil(t, err) + require.Equal(t, 1, len(res.CountRes)) + require.Equal(t, 20, res.CountRes[0].Value) }) t.Run("drop tables again", func(t *testing.T) { diff --git a/internal/core/repository/tx/tx_test.go b/internal/core/repository/tx/tx_test.go index 52d1bf1a..4472db79 100644 --- a/internal/core/repository/tx/tx_test.go +++ b/internal/core/repository/tx/tx_test.go @@ -7,7 +7,8 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" @@ -35,11 +36,11 @@ func initdb(t testing.TB) { ck = ch.Connect(ch.WithDSN(dsnCH), ch.WithAutoCreateDatabase(true), ch.WithPoolSize(16)) err = ck.Ping(ctx) - assert.Nil(t, err) + require.Nil(t, err) pg = bun.NewDB(sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))), pgdialect.New()) err = pg.Ping() - assert.Nil(t, err) + require.Nil(t, err) repo = tx.NewRepository(ck, pg) } @@ -47,10 +48,10 @@ func initdb(t testing.TB) { func createTables(t testing.TB) { _, err := pg.ExecContext(context.Background(), "CREATE TYPE account_status AS ENUM (?, ?, ?, ?)", core.Uninit, core.Active, core.Frozen, core.NonExist) - assert.False(t, err != nil && !strings.Contains(err.Error(), "already exists")) + require.False(t, err != nil && !strings.Contains(err.Error(), "already exists")) err = tx.CreateTables(context.Background(), ck, pg) - assert.Nil(t, err) + require.Nil(t, err) } func dropTables(t testing.TB) { @@ -58,12 +59,12 @@ func dropTables(t testing.TB) { defer cancel() _, err := ck.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.Transaction)(nil)).IfExists().Exec(ctx) - assert.Nil(t, err) + require.Nil(t, err) _, err = pg.ExecContext(ctx, "DROP TYPE IF EXISTS account_status") - assert.Nil(t, err) + require.Nil(t, err) } func TestRepository_AddTransactions(t *testing.T) { @@ -75,7 +76,7 @@ func TestRepository_AddTransactions(t *testing.T) { defer cancel() dbtx, err := pg.Begin() - assert.Nil(t, err) + require.Nil(t, err) t.Run("drop tables", func(t *testing.T) { dropTables(t) @@ -87,23 +88,23 @@ func TestRepository_AddTransactions(t *testing.T) { t.Run("add transactions", func(t *testing.T) { err := repo.AddTransactions(ctx, dbtx, transactions) - assert.Nil(t, err) + require.Nil(t, err) got := new(core.Transaction) err = dbtx.NewSelect().Model(got).Where("hash = ?", transactions[0].Hash).Scan(ctx) - assert.Nil(t, err) - assert.Equal(t, transactions[0], got) + require.Nil(t, err) + require.Equal(t, transactions[0], got) err = ck.NewSelect().Model(got).Where("hash = ?", transactions[0].Hash).Scan(ctx) - assert.Nil(t, err) + require.Nil(t, err) got.CreatedAt = transactions[0].CreatedAt // TODO: look at time.Time ch unmarshal - assert.Equal(t, transactions[0], got) + require.Equal(t, transactions[0], got) }) t.Run("commit transaction", func(t *testing.T) { err := dbtx.Commit() - assert.Nil(t, err) + require.Nil(t, err) }) t.Run("drop tables again", func(t *testing.T) { From df8a8494079c7c29ca261ad6bbe94d4df43fed29 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 10 Oct 2023 22:07:08 +0530 Subject: [PATCH 006/186] [blockRepo.filterBlocks] fix sloppyReassign: --- internal/core/repository/block/filter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index 3769fd0c..e336214e 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -133,7 +133,7 @@ func (r *Repository) filterBlocks(ctx context.Context, f *filter.BlocksReq) (ret return nil, err } - if err = r.countTransactions(ctx, ret); err != nil { + if err := r.countTransactions(ctx, ret); err != nil { return nil, err } From ee79131d95509b3a81bf2a1c3502568e4ae18e96 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 12:33:53 +0530 Subject: [PATCH 007/186] [repo] msg: filter by src/dst workchains --- api/http/docs.go | 12 ++++++++++++ api/http/swagger.json | 12 ++++++++++++ api/http/swagger.yaml | 8 ++++++++ internal/api/http/controller.go | 2 ++ internal/core/filter/msg.go | 3 +++ internal/core/repository/msg/filter.go | 12 ++++++++++++ 6 files changed, 49 insertions(+) diff --git a/api/http/docs.go b/api/http/docs.go index 3ebe70d7..f8980d6e 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -443,6 +443,18 @@ const docTemplate = `{ "name": "hash", "in": "query" }, + { + "type": "integer", + "description": "filter by source workchain", + "name": "src_workchain", + "in": "query" + }, + { + "type": "integer", + "description": "filter by destination workchain", + "name": "dst_workchain", + "in": "query" + }, { "type": "array", "items": { diff --git a/api/http/swagger.json b/api/http/swagger.json index 81125f2c..c930208d 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -440,6 +440,18 @@ "name": "hash", "in": "query" }, + { + "type": "integer", + "description": "filter by source workchain", + "name": "src_workchain", + "in": "query" + }, + { + "type": "integer", + "description": "filter by destination workchain", + "name": "dst_workchain", + "in": "query" + }, { "type": "array", "items": { diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 455bdf17..d270e98d 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -996,6 +996,14 @@ paths: in: query name: hash type: string + - description: filter by source workchain + in: query + name: src_workchain + type: integer + - description: filter by destination workchain + in: query + name: dst_workchain + type: integer - description: source address in: query items: diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 08591583..bb5a9853 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -555,6 +555,8 @@ func (c *Controller) AggregateTransactionsHistory(ctx *gin.Context) { // @Accept json // @Produce json // @Param hash query string false "msg hash" +// @Param src_workchain query int32 false "filter by source workchain" +// @Param dst_workchain query int32 false "filter by destination workchain" // @Param src_address query []string false "source address" // @Param dst_address query []string false "destination address" // @Param operation_id query string false "operation id in hex format or as int32" diff --git a/internal/core/filter/msg.go b/internal/core/filter/msg.go index 16c22fa4..acc1fed3 100644 --- a/internal/core/filter/msg.go +++ b/internal/core/filter/msg.go @@ -17,6 +17,9 @@ type MessagesReq struct { DstAddresses []*addr.Address // `form:"dst_address"` OperationID *uint32 + 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"` diff --git a/internal/core/repository/msg/filter.go b/internal/core/repository/msg/filter.go index 896a0056..2dd43e01 100644 --- a/internal/core/repository/msg/filter.go +++ b/internal/core/repository/msg/filter.go @@ -28,6 +28,12 @@ func (r *Repository) filterMsg(ctx context.Context, req *filter.MessagesReq) (re if len(req.DstAddresses) > 0 { q = q.Where("dst_address in (?)", bun.In(req.DstAddresses)) } + if req.SrcWorkchain != nil { + q = q.Where("src_workchain = ?", *req.SrcWorkchain) + } + if req.DstWorkchain != nil { + q = q.Where("dst_workchain = ?", *req.DstWorkchain) + } if req.OperationID != nil { q = q.Where("operation_id = ?", *req.OperationID) } @@ -75,6 +81,12 @@ func (r *Repository) countMsg(ctx context.Context, req *filter.MessagesReq) (int if len(req.DstAddresses) > 0 { q = q.Where("dst_address in (?)", ch.In(req.DstAddresses)) } + if req.SrcWorkchain != nil { + q = q.Where("src_workchain = ?", *req.SrcWorkchain) + } + if req.DstWorkchain != nil { + q = q.Where("dst_workchain = ?", *req.DstWorkchain) + } if req.OperationID != nil { q = q.Where("operation_id = ?", *req.OperationID) } From 49cae8acd4e0a6fbc2e726f19e2a6b4b8d2f6b37 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 12:45:01 +0530 Subject: [PATCH 008/186] [repo] msg history: filter by src/dst workchains --- api/http/docs.go | 14 +++++++++++++- api/http/swagger.json | 14 +++++++++++++- api/http/swagger.yaml | 10 +++++++++- internal/api/http/controller.go | 4 +++- internal/core/aggregate/history/msg.go | 3 +++ internal/core/repository/msg/history.go | 6 ++++++ 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index f8980d6e..7b96c55d 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -637,6 +637,18 @@ const docTemplate = `{ "name": "dst_address", "in": "query" }, + { + "type": "integer", + "description": "source workchain", + "name": "src_workchain", + "in": "query" + }, + { + "type": "integer", + "description": "destination workchain", + "name": "dst_workchain", + "in": "query" + }, { "type": "array", "items": { @@ -660,7 +672,7 @@ const docTemplate = `{ "items": { "type": "string" }, - "description": "filter by contract operation names", + "description": "contract operation names", "name": "operation_name", "in": "query" }, diff --git a/api/http/swagger.json b/api/http/swagger.json index c930208d..7488e1ee 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -634,6 +634,18 @@ "name": "dst_address", "in": "query" }, + { + "type": "integer", + "description": "source workchain", + "name": "src_workchain", + "in": "query" + }, + { + "type": "integer", + "description": "destination workchain", + "name": "dst_workchain", + "in": "query" + }, { "type": "array", "items": { @@ -657,7 +669,7 @@ "items": { "type": "string" }, - "description": "filter by contract operation names", + "description": "contract operation names", "name": "operation_name", "in": "query" }, diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index d270e98d..7346fc74 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -1128,6 +1128,14 @@ paths: type: string name: dst_address type: array + - description: source workchain + in: query + name: src_workchain + type: integer + - description: destination workchain + in: query + name: dst_workchain + type: integer - description: source contract interface in: query items: @@ -1140,7 +1148,7 @@ paths: type: string name: dst_contract type: array - - description: filter by contract operation names + - description: contract operation names in: query items: type: string diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index bb5a9853..31b13db8 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -677,9 +677,11 @@ func (c *Controller) AggregateMessages(ctx *gin.Context) { // @Param metric query string true "metric to show" Enums(message_count, message_amount_sum) // @Param src_address query []string false "source address" // @Param dst_address query []string false "destination address" +// @Param src_workchain query int32 false "source workchain" +// @Param dst_workchain query int32 false "destination workchain" // @Param src_contract query []string false "source contract interface" // @Param dst_contract query []string false "destination contract interface" -// @Param operation_name query []string false "filter by contract operation names" +// @Param operation_name query []string false "contract operation names" // @Param minter_address query string false "filter FT or NFT operations by minter address" // @Param from query string false "from timestamp" // @Param to query string false "to timestamp" diff --git a/internal/core/aggregate/history/msg.go b/internal/core/aggregate/history/msg.go index 58ee5d45..2583055a 100644 --- a/internal/core/aggregate/history/msg.go +++ b/internal/core/aggregate/history/msg.go @@ -19,6 +19,9 @@ type MessagesReq struct { SrcAddresses []*addr.Address // `form:"src_address"` DstAddresses []*addr.Address // `form:"dst_address"` + SrcWorkchain *int32 `form:"src_workchain"` + DstWorkchain *int32 `form:"dst_workchain"` + SrcContracts []string `form:"src_contract"` DstContracts []string `form:"dst_contract"` diff --git a/internal/core/repository/msg/history.go b/internal/core/repository/msg/history.go index bc705af8..5d8d3fc9 100644 --- a/internal/core/repository/msg/history.go +++ b/internal/core/repository/msg/history.go @@ -23,6 +23,12 @@ func (r *Repository) AggregateMessagesHistory(ctx context.Context, req *history. if len(req.DstAddresses) > 0 { q = q.Where("dst_address in (?)", ch.In(req.DstAddresses)) } + if req.SrcWorkchain != nil { + q = q.Where("src_workchain = ?", *req.SrcWorkchain) + } + if req.DstWorkchain != nil { + q = q.Where("dst_workchain = ?", *req.DstWorkchain) + } if len(req.SrcContracts) > 0 { q = q.Where("src_contract IN (?)", ch.In(req.SrcContracts)) } From 534947f16bb1ecd35d4bf19df26850bba19e6557 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 13:43:27 +0530 Subject: [PATCH 009/186] [repo] account: extended statistics on address (#27) --- api/http/docs.go | 13 +++ api/http/swagger.json | 13 +++ api/http/swagger.yaml | 9 ++ internal/core/aggregate/account.go | 8 ++ internal/core/repository/account/aggregate.go | 85 ++++++++++++++++--- 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index 7b96c55d..310d8632 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -1091,12 +1091,25 @@ const docTemplate = `{ } } }, + "owned_jetton_wallets": { + "type": "integer" + }, + "owned_nft_collections": { + "type": "integer" + }, + "owned_nft_items": { + "type": "integer" + }, "owners_count": { "type": "integer" }, "total_supply": { "$ref": "#/definitions/bunbig.Int" }, + "transactions_count": { + "description": "Address statistics", + "type": "integer" + }, "unique_owners": { "type": "array", "items": { diff --git a/api/http/swagger.json b/api/http/swagger.json index 7488e1ee..f17e0f10 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -1088,12 +1088,25 @@ } } }, + "owned_jetton_wallets": { + "type": "integer" + }, + "owned_nft_collections": { + "type": "integer" + }, + "owned_nft_items": { + "type": "integer" + }, "owners_count": { "type": "integer" }, "total_supply": { "$ref": "#/definitions/bunbig.Int" }, + "transactions_count": { + "description": "Address statistics", + "type": "integer" + }, "unique_owners": { "type": "array", "items": { diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 7346fc74..c40d1d53 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -128,10 +128,19 @@ definitions: type: array type: object type: array + owned_jetton_wallets: + type: integer + owned_nft_collections: + type: integer + owned_nft_items: + type: integer owners_count: type: integer total_supply: $ref: '#/definitions/bunbig.Int' + transactions_count: + description: Address statistics + type: integer unique_owners: items: properties: diff --git a/internal/core/aggregate/account.go b/internal/core/aggregate/account.go index 68c6986e..7276fc58 100644 --- a/internal/core/aggregate/account.go +++ b/internal/core/aggregate/account.go @@ -9,12 +9,20 @@ import ( ) type AccountsReq struct { + Address *addr.Address + MinterAddress *addr.Address // NFT or FT minter Limit int `form:"limit"` } type AccountsRes struct { + // Address statistics + TransactionsCount int `json:"transactions_count"` + OwnedNFTItems int `json:"owned_nft_items"` + OwnedNFTCollections int `json:"owned_nft_collections"` + OwnedJettonWallets int `json:"owned_jetton_wallets"` + // NFT minter Items int `json:"items,omitempty"` OwnersCount int `json:"owners_count,omitempty"` diff --git a/internal/core/repository/account/aggregate.go b/internal/core/repository/account/aggregate.go index d5e7b2c1..e4327994 100644 --- a/internal/core/repository/account/aggregate.go +++ b/internal/core/repository/account/aggregate.go @@ -13,6 +13,50 @@ import ( "github.com/tonindexer/anton/internal/core/aggregate" ) +func (r *Repository) aggregateAddressStatistics(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { + var err error + + res.TransactionsCount, err = r.ch.NewSelect(). + Model((*core.Transaction)(nil)). + Where("address = ?", req.Address). + Count(ctx) + if err != nil { + return errors.Wrap(err, "count transactions") + } + + err = r.ch.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("uniqExact(address)"). + Where("owner_address = ?", req.Address). + Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.NFTItem})). + Scan(ctx, &res.OwnedNFTItems) + if err != nil { + return errors.Wrap(err, "count owned nft items") + } + + err = r.ch.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("uniqExact(address)"). + Where("owner_address = ?", req.Address). + Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.NFTCollection})). + Scan(ctx, &res.OwnedNFTCollections) + if err != nil { + return errors.Wrap(err, "count owned nft collections") + } + + err = r.ch.NewSelect(). + Model((*core.AccountState)(nil)). + ColumnExpr("uniqExact(address)"). + Where("owner_address = ?", req.Address). + Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.JettonWallet})). + Scan(ctx, &res.OwnedJettonWallets) + if err != nil { + return errors.Wrap(err, "count owned jetton wallets") + } + + return nil +} + func (r *Repository) makeLastItemStateQuery(minter *addr.Address) *ch.SelectQuery { return r.ch.NewSelect(). Model((*core.AccountState)(nil)). @@ -103,15 +147,8 @@ func (r *Repository) aggregateFTMinter(ctx context.Context, req *aggregate.Accou return err } -func (r *Repository) AggregateAccounts(ctx context.Context, req *aggregate.AccountsReq) (*aggregate.AccountsRes, error) { - var ( - res aggregate.AccountsRes - interfaces []abi.ContractName - ) - - if req.MinterAddress == nil { - return nil, errors.Wrap(core.ErrInvalidArg, "minter address must be set") - } +func (r *Repository) aggregateMinterStatistics(ctx context.Context, req *aggregate.AccountsReq, res *aggregate.AccountsRes) error { + var interfaces []abi.ContractName err := r.ch.NewSelect(). Model((*core.AccountState)(nil)). @@ -120,22 +157,42 @@ func (r *Repository) AggregateAccounts(ctx context.Context, req *aggregate.Accou Group("address"). Scan(ctx, &interfaces) if err != nil { - return nil, err + return err } for _, t := range interfaces { switch t { case known.NFTCollection: - if err := r.aggregateNFTMinter(ctx, req, &res); err != nil { - return nil, err + if err := r.aggregateNFTMinter(ctx, req, res); err != nil { + return err } case known.JettonMinter: - if err := r.aggregateFTMinter(ctx, req, &res); err != nil { - return nil, err + if err := r.aggregateFTMinter(ctx, req, res); err != nil { + return err } } } + return nil +} + +func (r *Repository) AggregateAccounts(ctx context.Context, req *aggregate.AccountsReq) (*aggregate.AccountsRes, error) { + var res aggregate.AccountsRes + + if req.Address == nil && req.MinterAddress == nil { + return nil, errors.Wrap(core.ErrInvalidArg, "address must be set") + } + if req.Address != nil { + if err := r.aggregateAddressStatistics(ctx, req, &res); err != nil { + return nil, err + } + } + if req.MinterAddress != nil { + if err := r.aggregateMinterStatistics(ctx, req, &res); err != nil { + return nil, err + } + } + return &res, nil } From 05e0cc1012b1028a1e5b0546caac4123e428f4e5 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 13:48:09 +0530 Subject: [PATCH 010/186] [api] AggregateAccounts: add address param (#27) --- api/http/docs.go | 7 +++++++ api/http/swagger.json | 7 +++++++ api/http/swagger.yaml | 5 +++++ internal/api/http/controller.go | 7 +++++++ 4 files changed, 26 insertions(+) diff --git a/api/http/docs.go b/api/http/docs.go index 310d8632..c615f086 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -122,6 +122,13 @@ const docTemplate = `{ ], "summary": "aggregated account data", "parameters": [ + { + "type": "string", + "description": "address on which statistics are calculated", + "name": "address", + "in": "query", + "required": true + }, { "type": "string", "description": "NFT collection or FT master address", diff --git a/api/http/swagger.json b/api/http/swagger.json index f17e0f10..ac326128 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -119,6 +119,13 @@ ], "summary": "aggregated account data", "parameters": [ + { + "type": "string", + "description": "address on which statistics are calculated", + "name": "address", + "in": "query", + "required": true + }, { "type": "string", "description": "NFT collection or FT master address", diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index c40d1d53..56336a83 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -789,6 +789,11 @@ paths: - application/json description: Aggregates FT or NFT data filtered by minter address parameters: + - description: address on which statistics are calculated + in: query + name: address + required: true + type: string - description: NFT collection or FT master address in: query name: minter_address diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 31b13db8..6d73b514 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -373,6 +373,7 @@ func (c *Controller) GetAccounts(ctx *gin.Context) { // @Tags account // @Accept json // @Produce json +// @Param address query string true "address on which statistics are calculated" // @Param minter_address query string true "NFT collection or FT master address" // @Param limit query int false "limit" default(25) maximum(1000000) // @Success 200 {object} aggregate.AccountsRes @@ -390,6 +391,12 @@ func (c *Controller) AggregateAccounts(ctx *gin.Context) { return } + req.Address, err = unmarshalAddress(ctx.Query("address")) + if err != nil { + paramErr(ctx, "address", err) + return + } + req.MinterAddress, err = unmarshalAddress(ctx.Query("minter_address")) if err != nil { paramErr(ctx, "minter_address", err) From f4d42e8b8e97dfc4e55bccf79b8be1e8848a57d0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 13:52:12 +0530 Subject: [PATCH 011/186] [api] AggregateAccounts: omitempty for general account statistics (#27) --- internal/core/aggregate/account.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/core/aggregate/account.go b/internal/core/aggregate/account.go index 7276fc58..17e07c7c 100644 --- a/internal/core/aggregate/account.go +++ b/internal/core/aggregate/account.go @@ -18,10 +18,10 @@ type AccountsReq struct { type AccountsRes struct { // Address statistics - TransactionsCount int `json:"transactions_count"` - OwnedNFTItems int `json:"owned_nft_items"` - OwnedNFTCollections int `json:"owned_nft_collections"` - OwnedJettonWallets int `json:"owned_jetton_wallets"` + TransactionsCount int `json:"transactions_count,omitempty"` + OwnedNFTItems int `json:"owned_nft_items,omitempty"` + OwnedNFTCollections int `json:"owned_nft_collections,omitempty"` + OwnedJettonWallets int `json:"owned_jetton_wallets,omitempty"` // NFT minter Items int `json:"items,omitempty"` From a25b6b8405c01d3398de2dd28d19d3cfbd93ae43 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 13:55:29 +0530 Subject: [PATCH 012/186] [api] AggregateAccounts: do not require both addresses (#27) --- api/http/docs.go | 6 ++---- api/http/swagger.json | 6 ++---- api/http/swagger.yaml | 2 -- internal/api/http/controller.go | 4 ++-- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index c615f086..c62bd5e6 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -126,15 +126,13 @@ const docTemplate = `{ "type": "string", "description": "address on which statistics are calculated", "name": "address", - "in": "query", - "required": true + "in": "query" }, { "type": "string", "description": "NFT collection or FT master address", "name": "minter_address", - "in": "query", - "required": true + "in": "query" }, { "maximum": 1000000, diff --git a/api/http/swagger.json b/api/http/swagger.json index ac326128..08c21506 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -123,15 +123,13 @@ "type": "string", "description": "address on which statistics are calculated", "name": "address", - "in": "query", - "required": true + "in": "query" }, { "type": "string", "description": "NFT collection or FT master address", "name": "minter_address", - "in": "query", - "required": true + "in": "query" }, { "maximum": 1000000, diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 56336a83..a89e4087 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -792,12 +792,10 @@ paths: - description: address on which statistics are calculated in: query name: address - required: true type: string - description: NFT collection or FT master address in: query name: minter_address - required: true type: string - default: 25 description: limit diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 6d73b514..46cc084e 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -373,8 +373,8 @@ func (c *Controller) GetAccounts(ctx *gin.Context) { // @Tags account // @Accept json // @Produce json -// @Param address query string true "address on which statistics are calculated" -// @Param minter_address query string true "NFT collection or FT master address" +// @Param address query string false "address on which statistics are calculated" +// @Param minter_address query string false "NFT collection or FT master address" // @Param limit query int false "limit" default(25) maximum(1000000) // @Success 200 {object} aggregate.AccountsRes // @Router /accounts/aggregated [get] From e8a77daec89149894954f2693d87ee2719e2381c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 11 Oct 2023 14:05:53 +0530 Subject: [PATCH 013/186] [repo] account: optimize aggregateAddressStatistics (#27) --- internal/core/repository/account/aggregate.go | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/internal/core/repository/account/aggregate.go b/internal/core/repository/account/aggregate.go index e4327994..38d2ec20 100644 --- a/internal/core/repository/account/aggregate.go +++ b/internal/core/repository/account/aggregate.go @@ -2,6 +2,7 @@ package account import ( "context" + "database/sql" "github.com/pkg/errors" "github.com/uptrace/go-clickhouse/ch" @@ -24,34 +25,32 @@ func (r *Repository) aggregateAddressStatistics(ctx context.Context, req *aggreg return errors.Wrap(err, "count transactions") } - err = r.ch.NewSelect(). - Model((*core.AccountState)(nil)). - ColumnExpr("uniqExact(address)"). - Where("owner_address = ?", req.Address). - Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.NFTItem})). - Scan(ctx, &res.OwnedNFTItems) - if err != nil { - return errors.Wrap(err, "count owned nft items") + var countByInterfaces []struct { + Types []abi.ContractName + Count int } - err = r.ch.NewSelect(). Model((*core.AccountState)(nil)). - ColumnExpr("uniqExact(address)"). + Column("types"). + ColumnExpr("uniqExact(address) as count"). Where("owner_address = ?", req.Address). - Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.NFTCollection})). - Scan(ctx, &res.OwnedNFTCollections) + Group("types"). + Scan(ctx, &countByInterfaces) if err != nil { - return errors.Wrap(err, "count owned nft collections") + return errors.Wrap(err, "count owned nft items") } - err = r.ch.NewSelect(). - Model((*core.AccountState)(nil)). - ColumnExpr("uniqExact(address)"). - Where("owner_address = ?", req.Address). - Where("hasAny(types, [?])", ch.In([]abi.ContractName{known.JettonWallet})). - Scan(ctx, &res.OwnedJettonWallets) - if err != nil { - return errors.Wrap(err, "count owned jetton wallets") + for _, x := range countByInterfaces { + for _, t := range x.Types { + switch t { + case known.NFTItem: + res.OwnedNFTItems += x.Count + case known.NFTCollection: + res.OwnedNFTCollections += x.Count + case known.JettonWallet: + res.OwnedJettonWallets += x.Count + } + } } return nil @@ -156,6 +155,9 @@ func (r *Repository) aggregateMinterStatistics(ctx context.Context, req *aggrega Where("address = ?", req.MinterAddress). Group("address"). Scan(ctx, &interfaces) + if errors.Is(err, sql.ErrNoRows) { + return nil + } if err != nil { return err } From ada2db52d5eb76a578cb265e3b68cfd2e699db65 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 12 Oct 2023 12:53:11 +0530 Subject: [PATCH 014/186] update tonutils-go to v1.8.4 --- cmd/archive/archive.go | 2 +- cmd/indexer/indexer.go | 2 +- cmd/web/web.go | 2 +- go.mod | 4 +--- go.sum | 9 ++------- internal/app/fetcher.go | 4 ++-- internal/app/fetcher/block.go | 2 +- internal/app/fetcher/fetcher_test.go | 2 +- internal/app/fetcher/map.go | 12 ++++++------ internal/app/indexer.go | 2 +- internal/app/parser.go | 11 +++-------- internal/app/query.go | 2 +- internal/core/repository/block/filter.go | 4 ++++ internal/core/repository/msg/history.go | 15 +++++++++------ internal/core/tx.go | 2 +- 15 files changed, 35 insertions(+), 40 deletions(-) diff --git a/cmd/archive/archive.go b/cmd/archive/archive.go index 712550ac..799d0232 100644 --- a/cmd/archive/archive.go +++ b/cmd/archive/archive.go @@ -52,7 +52,7 @@ var Command = &cli.Command{ continue } - api := ton.NewAPIClient(client) + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() master, err := api.GetMasterchainInfo(ctx.Context) if err != nil { diff --git a/cmd/indexer/indexer.go b/cmd/indexer/indexer.go index 258d8acd..1a352ccc 100644 --- a/cmd/indexer/indexer.go +++ b/cmd/indexer/indexer.go @@ -44,7 +44,7 @@ var Command = &cli.Command{ } client := liteclient.NewConnectionPool() - api := ton.NewAPIClient(client) + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { split := strings.Split(addr, "|") if len(split) != 2 { diff --git a/cmd/web/web.go b/cmd/web/web.go index a9d50135..2632ddee 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -34,7 +34,7 @@ var Command = &cli.Command{ } client := liteclient.NewConnectionPool() - api := ton.NewAPIClient(client) + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { split := strings.Split(addr, "|") if len(split) != 2 { diff --git a/go.mod b/go.mod index c70d7f6e..d854f7b5 100644 --- a/go.mod +++ b/go.mod @@ -22,13 +22,11 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.7.4-0.20230602073040-7236a8d2ed40 + github.com/xssnick/tonutils-go v1.8.4 ) require github.com/gin-contrib/cors v1.4.0 -require github.com/alecthomas/participle/v2 v2.0.0-beta.5 // indirect - require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect diff --git a/go.sum b/go.sum index aa6a9bde..648d64db 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,6 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= -github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= -github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= -github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= -github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs= 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= @@ -70,7 +66,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0 h1:0/RkLzp6whu/zFCBlEgQyAkbj4QbOlObn8jYQuhm4vg= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0/go.mod h1:ZkFYp+b3tn7YiHR6yMnHqGetPfFZhbVYVTsTGBIbdCY= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= @@ -189,8 +184,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.7.4-0.20230602073040-7236a8d2ed40 h1:TeTl/CaO8iooltbSOSdyO75TL3h+fkMlvI5mFinmK68= -github.com/xssnick/tonutils-go v1.7.4-0.20230602073040-7236a8d2ed40/go.mod h1:wH8ldhLueyfXW15r3MyaIq9YzA+8bzvL6UMU2BLp08g= +github.com/xssnick/tonutils-go v1.8.4 h1:RdMau+dNRMC/N4IvceFKnOK0xLYwxf4EuYf/rzGYVvY= +github.com/xssnick/tonutils-go v1.8.4/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= diff --git a/internal/app/fetcher.go b/internal/app/fetcher.go index d6118001..a316978e 100644 --- a/internal/app/fetcher.go +++ b/internal/app/fetcher.go @@ -12,7 +12,7 @@ import ( ) type FetcherConfig struct { - API *ton.APIClient + API ton.APIClientWrapped Parser ParserService } @@ -26,7 +26,7 @@ func TimeTrack(start time.Time, fun string, args ...any) { } type FetcherService interface { - LookupMaster(ctx context.Context, api ton.APIClientWaiter, seqNo uint32) (*ton.BlockIDExt, error) + LookupMaster(ctx context.Context, api ton.APIClientWrapped, seqNo uint32) (*ton.BlockIDExt, error) UnseenBlocks(ctx context.Context, masterSeqNo uint32) (master *ton.BlockIDExt, shards []*ton.BlockIDExt, err error) UnseenShards(ctx context.Context, master *ton.BlockIDExt) (shards []*ton.BlockIDExt, err error) BlockTransactions(ctx context.Context, b *ton.BlockIDExt) ([]*core.Transaction, error) diff --git a/internal/app/fetcher/block.go b/internal/app/fetcher/block.go index 1cfb14b0..12cda5ce 100644 --- a/internal/app/fetcher/block.go +++ b/internal/app/fetcher/block.go @@ -9,7 +9,7 @@ import ( "github.com/xssnick/tonutils-go/ton" ) -func (s *Service) LookupMaster(ctx context.Context, api ton.APIClientWaiter, seqNo uint32) (*ton.BlockIDExt, error) { +func (s *Service) LookupMaster(ctx context.Context, api ton.APIClientWrapped, seqNo uint32) (*ton.BlockIDExt, error) { if master, ok := s.blocks.getMaster(seqNo); ok { return master, nil } diff --git a/internal/app/fetcher/fetcher_test.go b/internal/app/fetcher/fetcher_test.go index 879ddc3a..e6c6cc47 100644 --- a/internal/app/fetcher/fetcher_test.go +++ b/internal/app/fetcher/fetcher_test.go @@ -40,7 +40,7 @@ func newService(t *testing.T) *Service { if err != nil { t.Fatal(err) } - api := ton.NewAPIClient(client) + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() return NewService(&app.FetcherConfig{ API: api, diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index 3e81631e..c513a9f9 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -29,7 +29,7 @@ func MapAccount(b *ton.BlockIDExt, acc *tlb.Account) *core.AccountState { ret.Address = *addr.MustFromTonutils(acc.State.Address) } ret.Status = core.AccountStatus(acc.State.Status) - ret.Balance = bunbig.FromMathBig(acc.State.Balance.NanoTON()) + ret.Balance = bunbig.FromMathBig(acc.State.Balance.Nano()) ret.StateHash = acc.State.StateHash } if acc.Data != nil { @@ -58,11 +58,11 @@ func mapMessageInternal(msg *core.Message, raw *tlb.InternalMessage) error { msg.Bounce = raw.Bounce msg.Bounced = raw.Bounced - msg.Amount = bunbig.FromMathBig(raw.Amount.NanoTON()) + msg.Amount = bunbig.FromMathBig(raw.Amount.Nano()) msg.IHRDisabled = raw.IHRDisabled - msg.IHRFee = bunbig.FromMathBig(raw.IHRFee.NanoTON()) - msg.FwdFee = bunbig.FromMathBig(raw.FwdFee.NanoTON()) + msg.IHRFee = bunbig.FromMathBig(raw.IHRFee.Nano()) + msg.FwdFee = bunbig.FromMathBig(raw.FwdFee.Nano()) msg.Body = raw.Body.ToBOC() msg.BodyHash = raw.Body.Hash() @@ -156,7 +156,7 @@ func mapMessage(tx *tlb.Transaction, message tlb.Message) (*core.Message, error) err error ) - msgCell, err := tlb.ToCell(message) + msgCell, err := tlb.ToCell(message.Msg) if err != nil { return nil, errors.Wrap(err, "cannot convert message to cell") } @@ -229,7 +229,7 @@ func mapTransaction(b *ton.BlockIDExt, raw *tlb.Transaction) (*core.Transaction, InAmount: bunbig.NewInt(), OutAmount: bunbig.NewInt(), - TotalFees: bunbig.FromMathBig(raw.TotalFees.Coins.NanoTON()), + TotalFees: bunbig.FromMathBig(raw.TotalFees.Coins.Nano()), OrigStatus: core.AccountStatus(raw.OrigStatus), EndStatus: core.AccountStatus(raw.EndStatus), diff --git a/internal/app/indexer.go b/internal/app/indexer.go index 31c18114..4b5446da 100644 --- a/internal/app/indexer.go +++ b/internal/app/indexer.go @@ -9,7 +9,7 @@ import ( type IndexerConfig struct { DB *repository.DB - API *ton.APIClient + API ton.APIClientWrapped Fetcher FetcherService Parser ParserService diff --git a/internal/app/parser.go b/internal/app/parser.go index 5443bfbf..ed6594f8 100644 --- a/internal/app/parser.go +++ b/internal/app/parser.go @@ -21,7 +21,7 @@ type ParserConfig struct { ContractRepo core.ContractRepository } -func GetBlockchainConfig(ctx context.Context, api *ton.APIClient) (*cell.Cell, error) { +func GetBlockchainConfig(ctx context.Context, api ton.APIClientWrapped) (*cell.Cell, error) { var res tl.Serializable b, err := api.GetMasterchainInfo(ctx) @@ -38,12 +38,7 @@ func GetBlockchainConfig(ctx context.Context, api *ton.APIClient) (*cell.Cell, e case ton.ConfigAll: var state tlb.ShardStateUnsplit - c, err := cell.FromBOC(t.ConfigProof) - if err != nil { - return nil, err - } - - ref, err := c.BeginParse().LoadRef() + ref, err := t.ConfigProof.BeginParse().LoadRef() if err != nil { return nil, err } @@ -59,7 +54,7 @@ func GetBlockchainConfig(ctx context.Context, api *ton.APIClient) (*cell.Cell, e return nil, err } - return mcStateExtra.ConfigParams.Config.ToCell() + return mcStateExtra.ConfigParams.Config.Params.ToCell() case ton.LSError: return nil, t diff --git a/internal/app/query.go b/internal/app/query.go index 4a2fd217..b449bb02 100644 --- a/internal/app/query.go +++ b/internal/app/query.go @@ -15,7 +15,7 @@ import ( type QueryConfig struct { DB *repository.DB - API *ton.APIClient + API ton.APIClientWrapped } type QueryService interface { diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index e336214e..9631829c 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -29,6 +29,10 @@ func loadTransactions(q *bun.SelectQuery, prefix string, f *filter.BlocksReq) *b } func (r *Repository) countTransactions(ctx context.Context, ret []*core.Block) error { + if len(ret) == 0 { + return nil + } + var blockIDs [][]int64 for _, m := range ret { for _, s := range m.Shards { diff --git a/internal/core/repository/msg/history.go b/internal/core/repository/msg/history.go index 5d8d3fc9..ca8c25c1 100644 --- a/internal/core/repository/msg/history.go +++ b/internal/core/repository/msg/history.go @@ -11,12 +11,7 @@ import ( "github.com/tonindexer/anton/internal/core/aggregate/history" ) -func (r *Repository) AggregateMessagesHistory(ctx context.Context, req *history.MessagesReq) (*history.MessagesRes, error) { - var res history.MessagesRes - var bigIntRes bool // do we need to count account_data or account_states - - q := r.ch.NewSelect().Model((*core.Message)(nil)) - +func addMessagesHistoryFilters(q *ch.SelectQuery, req *history.MessagesReq) *ch.SelectQuery { if len(req.SrcAddresses) > 0 { q = q.Where("src_address in (?)", ch.In(req.SrcAddresses)) } @@ -41,6 +36,14 @@ func (r *Repository) AggregateMessagesHistory(ctx context.Context, req *history. if req.MinterAddress != nil { q = q.Where("minter_address = ?", req.MinterAddress) } + return q +} + +func (r *Repository) AggregateMessagesHistory(ctx context.Context, req *history.MessagesReq) (*history.MessagesRes, error) { + var res history.MessagesRes + var bigIntRes bool // do we need to count account_data or account_states + + q := addMessagesHistoryFilters(r.ch.NewSelect().Model((*core.Message)(nil)), req) switch req.Metric { case history.MessageCount: diff --git a/internal/core/tx.go b/internal/core/tx.go index cc7d74ae..a4175516 100644 --- a/internal/core/tx.go +++ b/internal/core/tx.go @@ -59,7 +59,7 @@ func (tx *Transaction) LoadDescription() error { // TODO: optionally load descri return errors.Wrap(err, "load description boc") } - if err := d.LoadFromCell(c.BeginParse()); err != nil { + if err := tlb.LoadFromCell(&d, c.BeginParse()); err != nil { return errors.Wrap(err, "load description from cell") } From 6a22ee0b59f1369f8ca79a4d953ae181b4da44ab Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 12 Oct 2023 14:46:37 +0530 Subject: [PATCH 015/186] [abi] apply definitions to get-methods --- .golangci.yaml | 2 +- abi/README.md | 260 +++++++++++++++++++++----------------- abi/abi.go | 16 +-- abi/get.go | 44 +++++-- abi/get_emulator.go | 65 ++++++---- abi/tlb.go | 60 ++++++++- abi/tlb_types.go | 33 +++-- cmd/contract/interface.go | 1 + internal/app/parser/tx.go | 30 +---- 9 files changed, 307 insertions(+), 204 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 6e346113..a16e7933 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -55,7 +55,7 @@ linters: - whitespace linters-settings: gocyclo: - min-complexity: 16 + min-complexity: 17 gosec: excludes: - G404 diff --git a/abi/README.md b/abi/README.md index a7bb1841..e04cbae7 100644 --- a/abi/README.md +++ b/abi/README.md @@ -12,13 +12,13 @@ you should define contract addresses in the network or a contract code Bag of Ce ```json5 { - "interface_name": "", // name of the contract - "addresses": [], // optional contract addresses - "code_boc": "", // optional contract code BoC - "definitions": {}, // map definition name to cell schema - "in_messages": [], // possible incoming messages schema - "out_messages": [], // possible outgoing messages schema - "get_methods": [] // get-method names, return values and arguments + "interface_name": "", // name of the contract + "addresses": [], // optional contract addresses + "code_boc": "", // optional contract code BoC + "definitions": {}, // map definition name to cell schema + "in_messages": [], // possible incoming messages schema + "out_messages": [], // possible outgoing messages schema + "get_methods": [] // get-method names, return values and arguments } ``` @@ -30,28 +30,28 @@ Also, it is possible to define similarly described embedded structures in each f ```json5 { - "op_name": "nft_start_auction", // operation name - "op_code": "0x5fcc3d14", // TL-B constructor prefix code (operation code) - "type": "external_out", // message type: internal, external_in, external_out - "body": [ - { // fields definitions - "name": "query_id", // field name - "tlb_type": "## 64", // field TL-B type - "format": "uint64" // describes how we should parse the field - }, - { - "name": "auction_config", - "tlb_type": "^", - "format": "struct", - "struct_fields": [ // fields of inner structure - { - "name": "beneficiary_address", - "tlb_type": "addr", - "format": "addr" - } - ] - } - ] + "op_name": "nft_start_auction", // operation name + "op_code": "0x5fcc3d14", // TL-B constructor prefix code (operation code) + "type": "external_out", // message type: internal, external_in, external_out + "body": [ + { // fields definitions + "name": "query_id", // field name + "tlb_type": "## 64", // field TL-B type + "format": "uint64" // describes how we should parse the field + }, + { + "name": "auction_config", + "tlb_type": "^", + "format": "struct", + "struct_fields": [ // fields of inner structure + { + "name": "beneficiary_address", + "tlb_type": "addr", + "format": "addr" + } + ] + } + ] } ``` @@ -89,96 +89,52 @@ Accepted types of `format`: 13. `string` - [string snake](https://github.com/xssnick/tonutils-go/blob/4d0157009913e35d450c36e28018cd0686502439/tvm/cell/builder.go#L317) is stored in the cell 14. `telemintText` - variable length string with [this](https://github.com/TelegramMessenger/telemint/blob/main/telemint.tlb#L25) TL-B constructor -### Shared TL-B constructors - -You can define some cell schema in contract interface `definitions` field and use it later in messages or contract data schemas. - -```json -{ - "interface_name": "telemint_nft_item", - "addresses": [ - "EQAOQdwdw8kGftJCSFgOErM1mBjYPe4DBPq8-AhF6vr9si5N", - "EQCA14o1-VWhS2efqoh_9M1b_A9DtKTuoqfmkn83AbJzwnPi" - ], - "definitions": { - "auction_config": [ - { - "name": "beneficiary_address", - "tlb_type": "addr" - } - ] - }, - "in_messages": [ - { - "op_name": "teleitem_start_auction", - "op_code": "0x487a8e81", - "body": [ - { - "name": "query_id", - "tlb_type": "## 64" - }, - { - "name": "auction_config", - "tlb_type": "^", - "format": "auction_config" - } - ] - } - ] -} -``` - -## Converting Golang struct to JSON schema - -You can convert Golang struct with described tlb tags to the JSON schema by using `abi.NewTLBDesc` and `abi.NewOperationDesc` functions. -See an example in [`tlb_test.go`](/abi/tlb_test.go) file. - ### Get-methods Each get-method consists of name (which is then used to get `method_id`), arguments and return values. ```json5 { - "interface_name": "jetton_minter", - "get_methods": [ - { - "name": "get_wallet_address", // get-method name - "arguments": [ - { - "name": "owner_address", // argument name - "stack_type": "slice", - "format": "addr" - } - ], - "return_values": [ - { - "name": "jetton_wallet_address", // return value name - "stack_type": "slice", // type we load - "format": "addr" // type we parse into - } - ] - }, - { - "name": "get_jetton_data", - "return_values": [ - { - "name": "total_supply", - "stack_type": "int", - "format": "bigInt" - }, - { - "name": "mintable", - "stack_type": "int", - "format": "bool" - }, - { - "name": "admin_address", - "stack_type": "slice", - "format": "addr" - } - ] - } - ] + "interface_name": "jetton_minter", + "get_methods": [ + { + "name": "get_wallet_address", // get-method name + "arguments": [ + { + "name": "owner_address", // argument name + "stack_type": "slice", + "format": "addr" + } + ], + "return_values": [ + { + "name": "jetton_wallet_address", // return value name + "stack_type": "slice", // type we load + "format": "addr" // type we parse into + } + ] + }, + { + "name": "get_jetton_data", + "return_values": [ + { + "name": "total_supply", + "stack_type": "int", + "format": "bigInt" + }, + { + "name": "mintable", + "stack_type": "int", + "format": "bool" + }, + { + "name": "admin_address", + "stack_type": "slice", + "format": "addr" + } + ] + } + ] } ``` @@ -205,6 +161,7 @@ Accepted types to map from or into in `format` field: 6. `string` - load string snake from cell 7. `bytes` - convert big int to bytes 8. `content` - load [TEP-64](https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md) standard token data into [`nft.ContentAny`](https://github.com/xssnick/tonutils-go/blob/b839942a7b7bc431cc610f2ca3d9ff0e03079586/ton/nft/content.go#L10) +9. `struct` - define struct_fields to parse cell ## Known contracts @@ -218,3 +175,80 @@ Accepted types to map from or into in `format` field: 8. [STON.fi](https://ston.fi) DEX: [architecture](https://docs.ston.fi/docs/developer-section/architecture), [contract code](https://github.com/ston-fi/dex-core) 9. [Megaton.fi](https://megaton.fi) DEX: [architecture](https://docs.megaton.fi/developers/contract) 10. [Tonpay](https://thetonpay.app): [go-sdk](https://github.com/TheTonpay/tonpay-go-sdk), [js-sdk](https://github.com/TheTonpay/tonpay-js-sdk) + +### Shared TL-B constructors + +You can define some cell schema in `definitions` field of contract interface. + +You can use those definitions in message schemas: + +```json +{ + "interface_name": "telemint_nft_item", + "addresses": [ + "EQAOQdwdw8kGftJCSFgOErM1mBjYPe4DBPq8-AhF6vr9si5N", + "EQCA14o1-VWhS2efqoh_9M1b_A9DtKTuoqfmkn83AbJzwnPi" + ], + "definitions": { + "auction_config": [ + { + "name": "beneficiary_address", + "tlb_type": "addr" + } + ] + }, + "in_messages": [ + { + "op_name": "teleitem_start_auction", + "op_code": "0x487a8e81", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64" + }, + { + "name": "auction_config", + "tlb_type": "^", + "format": "auction_config" + } + ] + } + ] +} +``` + +Or get-method stack values schemas: + +```json5 +{ + "interface_name": "amm", + "definitions": { + "amm_state": [ + { + "name": "quote_asset_reserve", + "tlb_type": ".", + "format": "coins" + }, + // ... + ] + }, + "get_methods": [ + { + "name": "get_amm_data", + "return_values": [ + // ... + { + "name": "amm_state", + "stack_type": "cell", + "format": "amm_state" + }, + ] + } + ] +} +``` + +## Converting Golang struct to JSON schema + +You can convert Golang struct with described tlb tags to the JSON schema by using `abi.NewTLBDesc` and `abi.NewOperationDesc` functions. +See an example in [`tlb_test.go`](/abi/tlb_test.go) file. diff --git a/abi/abi.go b/abi/abi.go index 0f925c8e..66a1485d 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -11,14 +11,14 @@ import ( type ContractName string type InterfaceDesc struct { - Name ContractName `json:"interface_name"` - Addresses []*addr.Address `json:"addresses,omitempty"` - CodeBoc string `json:"code_boc,omitempty"` - Definitions map[string]TLBFieldsDesc `json:"definitions,omitempty"` - InMessages []OperationDesc `json:"in_messages,omitempty"` - OutMessages []OperationDesc `json:"out_messages,omitempty"` - GetMethods []GetMethodDesc `json:"get_methods,omitempty"` - ContractData TLBFieldsDesc `json:"contract_data,omitempty"` + Name ContractName `json:"interface_name"` + Addresses []*addr.Address `json:"addresses,omitempty"` + CodeBoc string `json:"code_boc,omitempty"` + Definitions map[TLBType]TLBFieldsDesc `json:"definitions,omitempty"` + InMessages []OperationDesc `json:"in_messages,omitempty"` + OutMessages []OperationDesc `json:"out_messages,omitempty"` + GetMethods []GetMethodDesc `json:"get_methods,omitempty"` + ContractData TLBFieldsDesc `json:"contract_data,omitempty"` } func (i *InterfaceDesc) RegisterDefinitions() error { diff --git a/abi/get.go b/abi/get.go index e54b3713..b8492ca8 100644 --- a/abi/get.go +++ b/abi/get.go @@ -20,20 +20,11 @@ const ( VmSlice StackType = "slice" ) -// formats -const ( - VmAddr StackType = "addr" - VmBool StackType = "bool" - VmBigInt StackType = "bigInt" - VmString StackType = "string" - VmBytes StackType = "bytes" - VmContentCell StackType = "content" -) - type VmValueDesc struct { - Name string `json:"name"` - StackType StackType `json:"stack_type"` - Format StackType `json:"format,omitempty"` + Name string `json:"name"` + StackType StackType `json:"stack_type"` + Format TLBType `json:"format,omitempty"` + Fields TLBFieldsDesc `json:"struct_fields,omitempty"` // Format = "struct" } type GetMethodDesc struct { @@ -42,6 +33,33 @@ type GetMethodDesc struct { ReturnValues []VmValueDesc `json:"return_values"` } +func (desc *GetMethodDesc) MapRegisteredDefinitions() { + for i := range desc.Arguments { + if desc.Arguments[i].Format == TLBStructCell { + desc.Arguments[i].Fields.MapRegisteredDefinitions() + continue + } + d, ok := registeredDefinitions[desc.Arguments[i].Format] + if ok { + desc.Arguments[i].Format = TLBStructCell + desc.Arguments[i].Fields = d + continue + } + } + for i := range desc.ReturnValues { + if desc.ReturnValues[i].Format == TLBStructCell { + desc.ReturnValues[i].Fields.MapRegisteredDefinitions() + continue + } + d, ok := registeredDefinitions[desc.ReturnValues[i].Format] + if ok { + desc.ReturnValues[i].Format = TLBStructCell + desc.ReturnValues[i].Fields = d + continue + } + } +} + 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_emulator.go b/abi/get_emulator.go index 9212d799..b15f7630 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -14,6 +14,7 @@ import ( "github.com/tonkeeper/tongo/tvm" "github.com/xssnick/tonutils-go/address" + tutlb "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton/nft" "github.com/xssnick/tonutils-go/tvm/cell" ) @@ -81,7 +82,7 @@ func vmMakeValueInt(v *VmValue) (ret tlb.VmStackValue, _ error) { var ok bool switch v.Format { - case "", VmBigInt: + case "", TLBBigInt: bi, ok = v.Payload.(*big.Int) case "uint8": ui, uok := v.Payload.(uint8) @@ -126,9 +127,9 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { var ok bool switch v.Format { - case "", VmCell: + case "", TLBCell: c, ok = v.Payload.(*cell.Cell) - case VmAddr: + case TLBAddr: a, aok := v.Payload.(*address.Address) if aok { b := cell.BeginCell() @@ -137,7 +138,7 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { } c, ok = b.EndCell(), aok } - case VmString: + case TLBString: s, sok := v.Payload.(string) if sok { b := cell.BeginCell() @@ -146,6 +147,12 @@ func vmMakeValueCell(v *VmValue) (tlb.VmStackValue, error) { } c, ok = b.EndCell(), sok } + case TLBStructCell: + var err error + c, err = tutlb.ToCell(v.Payload) + if err != nil { + return tlb.VmStackValue{}, err + } } if !ok { return tlb.VmStackValue{}, errors.Wrapf(ErrWrongValueFormat, "'%s' type with '%s' format", v.StackType, v.Format) @@ -169,9 +176,9 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { var ok bool switch v.Format { - case "", VmSlice: + case "", TLBType(VmSlice): s, ok = v.Payload.(*cell.Slice) - case VmAddr: + case TLBAddr: a, aok := v.Payload.(*address.Address) if aok { b := cell.BeginCell() @@ -180,7 +187,7 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { } s, ok = b.EndCell().BeginParse(), aok } - case VmString: + case TLBString: a, aok := v.Payload.(string) if aok { b := cell.BeginCell() @@ -189,6 +196,12 @@ func vmMakeValueSlice(v *VmValue) (tlb.VmStackValue, error) { } s, ok = b.EndCell().BeginParse(), aok } + case TLBStructCell: + c, err := tutlb.ToCell(v.Payload) + if err != nil { + return tlb.VmStackValue{}, err + } + s = c.BeginParse() } if !ok { return tlb.VmStackValue{}, errors.Wrapf(ErrWrongValueFormat, "'%s' type with '%s' format", v.StackType, v.Format) @@ -237,7 +250,7 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } switch d.Format { - case "", VmBigInt: + case "", TLBBigInt: return bi, nil case "uint8": return uint8(bi.Uint64()), nil @@ -255,9 +268,9 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return int32(bi.Int64()), nil case "int64": return bi.Int64(), nil - case VmBool: + case TLBBool: return bi.Cmp(big.NewInt(0)) != 0, nil - case VmBytes: + case TLBBytes: return bi.Bytes(), nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) @@ -268,11 +281,11 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": switch d.Format { - case "", VmCell: + case "", TLBCell, TLBStructCell: return (*cell.Cell)(nil), nil - case VmString: + case TLBString: return "", nil - case VmContentCell: + case TLBContentCell: return nft.ContentAny(nil), nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) @@ -295,26 +308,32 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } switch d.Format { - case "", VmCell: + case "", TLBCell: return c, nil - case VmString: + case TLBString: s, err := c.BeginParse().LoadStringSnake() if err != nil { return nil, errors.Wrap(err, "load string snake") } return s, nil - case VmAddr: + case TLBAddr: a, err := c.BeginParse().LoadAddr() if err != nil { return nil, errors.Wrap(err, "load address") } return a, nil - case VmContentCell: + case TLBContentCell: content, err := nft.ContentFromCell(c) if err != nil { return nil, errors.Wrap(err, "load content from cell") } return content, nil + case TLBStructCell: + parsed, err := d.Fields.FromCell(c) + if err != nil { + return nil, errors.Wrapf(err, "load struct from cell on %s value description schema", d.Name) + } + return parsed, nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) } @@ -324,11 +343,11 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": switch d.Format { - case "", VmSlice: + case "": return (*cell.Slice)(nil), nil - case VmAddr: + case TLBAddr: return address.NewAddressNone(), nil - case VmString: + case TLBString: return "", nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) @@ -351,11 +370,11 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } switch d.Format { - case "", VmSlice: + case "": return c.BeginParse(), nil - case VmString: + case TLBString: return c.BeginParse().LoadStringSnake() - case VmAddr: + case TLBAddr: a, err := c.BeginParse().LoadAddr() if err != nil { return nil, errors.Wrap(err, "load address") diff --git a/abi/tlb.go b/abi/tlb.go index 1af2c2dc..8dabf9e0 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -18,7 +18,7 @@ import ( type TLBFieldDesc struct { Name string `json:"name"` Type string `json:"tlb_type"` - Format string `json:"format,omitempty"` + Format TLBType `json:"format,omitempty"` Optional bool `json:"optional,omitempty"` Fields TLBFieldsDesc `json:"struct_fields,omitempty"` // Format = "struct" } @@ -51,14 +51,14 @@ func tlbMakeDesc(t reflect.Type) (ret TLBFieldsDesc, err error) { schema.Format = ft case f.Type.Kind() == reflect.Pointer && f.Type.Elem().Kind() == reflect.Struct: - schema.Format = structTypeName + schema.Format = TLBStructCell schema.Fields, err = tlbMakeDesc(f.Type.Elem()) if err != nil { return nil, fmt.Errorf("%s: %w", f.Name, err) } case f.Type.Kind() == reflect.Struct: - schema.Format = structTypeName + schema.Format = TLBStructCell schema.Fields, err = tlbMakeDesc(f.Type) if err != nil { return nil, fmt.Errorf("%s: %w", f.Name, err) @@ -193,7 +193,7 @@ func tlbParseDesc(fields []reflect.StructField, schema TLBFieldsDesc, skipOption // get type from `format` field sf.Type, ok = typeNameMap[f.Format] if !ok { - if f.Format != "" && f.Format != structTypeName { + if f.Format != "" && f.Format != TLBStructCell { return nil, fmt.Errorf("unknown format '%s'", f.Format) } // parse tlb tag and get default type @@ -236,19 +236,43 @@ func (desc TLBFieldsDesc) New(skipOptional ...bool) (any, error) { func (desc TLBFieldsDesc) MapRegisteredDefinitions() { for i := range desc { - if desc[i].Format == structTypeName { + if desc[i].Format == TLBStructCell { desc[i].Fields.MapRegisteredDefinitions() continue } d, ok := registeredDefinitions[desc[i].Format] if ok { - desc[i].Format = structTypeName + desc[i].Format = TLBStructCell desc[i].Fields = d continue } } } +func (desc TLBFieldsDesc) FromCell(c *cell.Cell) (any, error) { + parsed, err := desc.New() + if err != nil { + return nil, errors.Wrapf(err, "creating struct") + } + if err := tlb.LoadFromCell(parsed, c.BeginParse()); err == nil { + return parsed, nil + } + if !strings.Contains(err.Error(), "not enough data in reader") && !strings.Contains(err.Error(), "no more refs exists") { + return nil, errors.Wrap(err, "load from cell") + } + + // skipping optional fields + parsed, err = desc.New(true) + if err != nil { + return nil, errors.Wrapf(err, "creating struct (skip optional)") + } + if err := tlb.LoadFromCell(parsed, c.BeginParse()); err != nil { + return nil, errors.Wrap(err, "load from cell (skip optional)") + } + + return parsed, nil +} + func operationID(t reflect.Type) (uint32, error) { op := t.Field(0) if op.Type != reflect.TypeOf(tlb.Magic{}) { @@ -312,3 +336,27 @@ func (desc *OperationDesc) New(skipOptional ...bool) (any, error) { func (desc *OperationDesc) MapRegisteredDefinitions() { desc.Body.MapRegisteredDefinitions() } + +func (desc *OperationDesc) FromCell(c *cell.Cell) (any, error) { + parsed, err := desc.New() + if err != nil { + return nil, errors.Wrapf(err, "creating struct") + } + if err = tlb.LoadFromCell(parsed, c.BeginParse()); err == nil { + return parsed, nil + } + if !strings.Contains(err.Error(), "not enough data in reader") && !strings.Contains(err.Error(), "no more refs exists") { + return nil, errors.Wrap(err, "load from cell") + } + + // skipping optional fields + parsed, err = desc.New(true) + if err != nil { + return nil, errors.Wrapf(err, "creating struct (skip optional)") + } + if err = tlb.LoadFromCell(parsed, c.BeginParse()); err != nil { + return nil, errors.Wrap(err, "load from cell (skip optional)") + } + + return parsed, nil +} diff --git a/abi/tlb_types.go b/abi/tlb_types.go index fe6b63f9..2fe47b79 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -11,7 +11,18 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" ) -const structTypeName = "struct" +type TLBType string + +const ( + TLBAddr TLBType = "addr" + TLBBool TLBType = "bool" + TLBBigInt TLBType = "bigInt" + TLBString TLBType = "string" + TLBBytes TLBType = "bytes" + TLBCell TLBType = "cell" + TLBContentCell TLBType = "content" + TLBStructCell TLBType = "struct" +) type TelemintText struct { Len uint8 // ## 8 @@ -47,11 +58,11 @@ func (x *StringSnake) LoadFromCell(loader *cell.Slice) error { } var ( - typeNameRMap = map[reflect.Type]string{ - reflect.TypeOf([]uint8{}): "bytes", + typeNameRMap = map[reflect.Type]TLBType{ + reflect.TypeOf([]uint8{}): TLBBytes, } - typeNameMap = map[string]reflect.Type{ - "bool": reflect.TypeOf(false), + typeNameMap = map[TLBType]reflect.Type{ + TLBBool: reflect.TypeOf(false), "int8": reflect.TypeOf(int8(0)), "int16": reflect.TypeOf(int16(0)), "int32": reflect.TypeOf(int32(0)), @@ -60,18 +71,18 @@ var ( "uint16": reflect.TypeOf(uint16(0)), "uint32": reflect.TypeOf(uint32(0)), "uint64": reflect.TypeOf(uint64(0)), - "bytes": reflect.TypeOf([]byte{}), - "bigInt": reflect.TypeOf(big.NewInt(0)), - "cell": reflect.TypeOf((*cell.Cell)(nil)), + TLBBytes: reflect.TypeOf([]byte{}), + TLBBigInt: reflect.TypeOf(big.NewInt(0)), + TLBCell: reflect.TypeOf((*cell.Cell)(nil)), "dict": reflect.TypeOf((*cell.Dictionary)(nil)), "magic": reflect.TypeOf(tlb.Magic{}), "coins": reflect.TypeOf(tlb.Coins{}), - "addr": reflect.TypeOf((*address.Address)(nil)), - "string": reflect.TypeOf((*StringSnake)(nil)), + TLBAddr: reflect.TypeOf((*address.Address)(nil)), + TLBString: reflect.TypeOf((*StringSnake)(nil)), "telemintText": reflect.TypeOf((*TelemintText)(nil)), } - registeredDefinitions = map[string]TLBFieldsDesc{} + registeredDefinitions = map[TLBType]TLBFieldsDesc{} ) func init() { diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index 010e6653..a607959f 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -116,6 +116,7 @@ func parseInterfaceDesc(d *abi.InterfaceDesc) (*core.ContractInterface, []*core. GetMethodsDesc: d.GetMethods, } for it := range i.GetMethodsDesc { + i.GetMethodsDesc[it].MapRegisteredDefinitions() i.GetMethodHashes = append(i.GetMethodHashes, abi.MethodNameHash(i.GetMethodsDesc[it].Name)) } if len(i.Code) == 0 { diff --git a/internal/app/parser/tx.go b/internal/app/parser/tx.go index 88c7522f..0f06c874 100644 --- a/internal/app/parser/tx.go +++ b/internal/app/parser/tx.go @@ -3,42 +3,14 @@ package parser import ( "context" "encoding/json" - "strings" "github.com/pkg/errors" - "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" ) -func parseBody(op *core.ContractOperation, payload *cell.Cell) (any, error) { - msgParsed, err := op.Schema.New() - if err != nil { - return nil, errors.Wrapf(err, "creating struct from %s/%s schema", op.ContractName, op.OperationName) - } - - if err = tlb.LoadFromCell(msgParsed, payload.BeginParse()); err == nil { - return msgParsed, nil - } - if !strings.Contains(err.Error(), "not enough data in reader") && !strings.Contains(err.Error(), "no more refs exists") { - return nil, errors.Wrap(err, "load from cell") - } - - // skipping optional fields - msgParsed, err = op.Schema.New(true) - if err != nil { - return nil, errors.Wrapf(err, "creating struct from %s/%s schema (skip optional)", op.ContractName, op.OperationName) - } - - if err = tlb.LoadFromCell(msgParsed, payload.BeginParse()); err != nil { - return nil, errors.Wrap(err, "load from cell (skip optional)") - } - - return msgParsed, nil -} - func (s *Service) parseDirectedMessage(ctx context.Context, acc *core.AccountState, msg *core.Message) error { if acc == nil { return errors.Wrap(app.ErrImpossibleParsing, "no account data") @@ -75,7 +47,7 @@ func (s *Service) parseDirectedMessage(ctx context.Context, acc *core.AccountSta return errors.Wrap(err, "msg body from boc") } - msgParsed, err := parseBody(op, payloadCell) + msgParsed, err := op.Schema.FromCell(payloadCell) if err != nil { return errors.Wrap(err, "msg body from boc") } From 1f5427c5f8cdd7d20dcc9b4f28316fec0ca85162 Mon Sep 17 00:00:00 2001 From: stfy Date: Thu, 12 Oct 2023 21:34:54 +0300 Subject: [PATCH 016/186] feat: add library support --- abi/get_emulator.go | 32 ++++++-- go.mod | 2 +- internal/app/fetcher/account.go | 31 ++++++++ internal/app/fetcher/cache.go | 42 ++++++++++ internal/app/fetcher/fetcher.go | 6 +- internal/app/fetcher/libraries.go | 77 +++++++++++++++++++ internal/app/fetcher/map.go | 2 - internal/app/parser/get.go | 7 +- internal/core/account.go | 1 + internal/core/tx.go | 2 +- ...007145251_account_state_libraries.down.sql | 5 ++ ...31007145251_account_state_libraries.up.sql | 1 + ...007145251_account_state_libraries.down.sql | 5 ++ ...31007145251_account_state_libraries.up.sql | 5 ++ 14 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 internal/app/fetcher/libraries.go create mode 100644 migrations/chmigrations/20231007145251_account_state_libraries.down.sql create mode 100644 migrations/chmigrations/20231007145251_account_state_libraries.up.sql create mode 100644 migrations/pgmigrations/20231007145251_account_state_libraries.down.sql create mode 100644 migrations/pgmigrations/20231007145251_account_state_libraries.up.sql diff --git a/abi/get_emulator.go b/abi/get_emulator.go index b15f7630..319062f4 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -4,6 +4,8 @@ import ( "context" "encoding/base64" "fmt" + "github.com/tonkeeper/tongo/ton" + "github.com/tonkeeper/tongo/txemulator" "math/big" "github.com/pkg/errors" @@ -46,14 +48,12 @@ type Emulator struct { } func newEmulator(addr *address.Address, e *tvm.Emulator) (*Emulator, error) { - err := e.SetVerbosityLevel(0) - if err != nil { - return nil, errors.Wrap(err, "set verbosity level") - } - accId, err := tongo.AccountIDFromBase64Url(addr.String()) + accId, err := ton.AccountIDFromBase64Url(addr.String()) + if err != nil { return nil, errors.Wrap(err, "parse address") } + return &Emulator{Emulator: e, AccountID: accId}, nil } @@ -69,11 +69,29 @@ func NewEmulator(addr *address.Address, code, data, cfg *cell.Cell) (*Emulator, return newEmulator(addr, e) } -func NewEmulatorBase64(addr *address.Address, code, data, cfg string) (*Emulator, error) { - e, err := tvm.NewEmulatorFromBOCsBase64(code, data, cfg) +func NewEmulatorBase64(addr *address.Address, code, data, cfg, libraries string) (*Emulator, error) { + var ( + e *tvm.Emulator + err error + ) + + if libraries != "" { + e, err = tvm.NewEmulatorFromBOCsBase64( + code, + data, + cfg, + tvm.WithLazyC7Optimization(), + tvm.WithLibrariesBase64(libraries), + tvm.WithVerbosityLevel(txemulator.PrintsAllStackValuesForCommand), + ) + } else { + e, err = tvm.NewEmulatorFromBOCsBase64(code, data, cfg, tvm.WithLazyC7Optimization(), tvm.WithVerbosityLevel(txemulator.PrintsAllStackValuesForCommand)) + } + if err != nil { return nil, err } + return newEmulator(addr, e) } diff --git a/go.mod b/go.mod index d854f7b5..1bfd9b2a 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.8.10 - github.com/tonkeeper/tongo v1.1.2 + github.com/tonkeeper/tongo v1.3.0 github.com/uptrace/bun v1.1.12 github.com/uptrace/bun/dialect/pgdialect v1.1.12 github.com/uptrace/bun/driver/pgdriver v1.1.12 diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index e3a22e50..54e138d4 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -2,6 +2,8 @@ package fetcher import ( "context" + "github.com/tonindexer/anton/abi" + "github.com/xssnick/tonutils-go/tvm/cell" "time" "github.com/pkg/errors" @@ -30,6 +32,35 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr } acc = MapAccount(b, raw) + + if raw.Code != nil { + libs, err := s.GetAccountLibraries(ctx, raw) + + if err != nil { + return nil, 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 nil, errors.Wrap(err, "get library hash") + } + + lib := s.libraries.get(hash) + + if lib != nil { + acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) + } + } else { + acc.GetMethodHashes, _ = abi.GetMethodHashes(raw.Code) + } + } + if acc.Status == core.NonExist { return nil, errors.Wrap(core.ErrNotFound, "account does not exists") } diff --git a/internal/app/fetcher/cache.go b/internal/app/fetcher/cache.go index eb51b264..620bf47e 100644 --- a/internal/app/fetcher/cache.go +++ b/internal/app/fetcher/cache.go @@ -1,6 +1,7 @@ package fetcher import ( + "encoding/hex" "sync" "time" @@ -121,3 +122,44 @@ func (c *accountCache) set(bExt *ton.BlockIDExt, acc *core.AccountState) { c.m[b][acc.Address] = acc c.clearCaches() } + +type librariesCache struct { + libs map[string]*LibDescription + lastCleared time.Time + sync.Mutex +} + +func newLibrariesCache() *librariesCache { + return &librariesCache{ + libs: map[string]*LibDescription{}, + lastCleared: time.Time{}, + } +} + +func (c *librariesCache) get(hash []byte) *LibDescription { + c.Lock() + defer c.Unlock() + + l, ok := c.libs[hex.EncodeToString(hash)] + + if ok { + return l + } + + return nil +} + +func (c *librariesCache) set(hash []byte, desc *LibDescription) { + c.Lock() + defer c.Unlock() + + h := hex.EncodeToString(hash) + + _, ok := c.libs[h] + + if ok { + return + } + + c.libs[h] = desc +} diff --git a/internal/app/fetcher/fetcher.go b/internal/app/fetcher/fetcher.go index 722ffcb2..06f01e7a 100644 --- a/internal/app/fetcher/fetcher.go +++ b/internal/app/fetcher/fetcher.go @@ -12,8 +12,9 @@ type Service struct { masterWorkchain int32 masterShard uint64 - accounts *accountCache - blocks *blocksCache + accounts *accountCache + blocks *blocksCache + libraries *librariesCache } func NewService(cfg *app.FetcherConfig) *Service { @@ -23,5 +24,6 @@ func NewService(cfg *app.FetcherConfig) *Service { masterShard: 0x8000000000000000, accounts: newAccountCache(), blocks: newBlocksCache(), + libraries: newLibrariesCache(), } } diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go new file mode 100644 index 00000000..a01c9fe5 --- /dev/null +++ b/internal/app/fetcher/libraries.go @@ -0,0 +1,77 @@ +package fetcher + +import ( + "context" + "github.com/pkg/errors" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +type LibDescription struct { + _ tlb.Magic `tlb:"$00"` + Lib *cell.Cell `tlb:"^"` + Publishers *cell.Dictionary `tlb:"dict inline 256"` +} + +func (s *Service) GetAccountLibraries(ctx context.Context, raw *tlb.Account) (*cell.Cell, error) { + hashes, err := findLibraries(raw.Code) + + if err != nil { + return nil, errors.Wrapf(err, "find libraries") + } + + libs, err := s.API.GetLibraries(ctx, hashes...) + + if err != nil { + return nil, errors.Wrapf(err, "get libraries") + } + + libsMap := cell.NewDict(256) + + for i, hash := range hashes { + desc := LibDescription{Lib: libs[i]} + + h := cell.BeginCell().MustStoreSlice(hash, 256).EndCell() + t, err := tlb.ToCell(desc) + + if err != nil { + return nil, err + } + + err = libsMap.Set(h, t) + s.libraries.set(hash, &desc) + + if err != nil { + return nil, err + } + } + + return libsMap.ToCell() +} + +func getLibraryHash(code *cell.Cell) ([]byte, error) { + hash, err := code.BeginParse().LoadBinarySnake() + + if err != nil { + return nil, err + } + + return hash[1:], nil +} + +// TODO recursive Refs +func findLibraries(code *cell.Cell) ([][]byte, error) { + hashes := make([][]byte, 0) + + if code.GetType() == cell.LibraryCellType { + hash, err := getLibraryHash(code) + + if err != nil { + return nil, err + } + + hashes = append(hashes, hash) + } + + return hashes, nil +} diff --git a/internal/app/fetcher/map.go b/internal/app/fetcher/map.go index c513a9f9..b7b82d42 100644 --- a/internal/app/fetcher/map.go +++ b/internal/app/fetcher/map.go @@ -10,7 +10,6 @@ import ( "github.com/xssnick/tonutils-go/ton" "github.com/xssnick/tonutils-go/tvm/cell" - "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" ) @@ -39,7 +38,6 @@ func MapAccount(b *ton.BlockIDExt, acc *tlb.Account) *core.AccountState { if acc.Code != nil { ret.Code = acc.Code.ToBOC() ret.CodeHash = acc.Code.Hash() - ret.GetMethodHashes, _ = abi.GetMethodHashes(acc.Code) } ret.LastTxLT = acc.LastTxLT ret.LastTxHash = acc.LastTxHash diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 20721eae..a4aae5ea 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -47,11 +47,12 @@ func (s *Service) callGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc * }) } - codeBase64, dataBase64 := + codeBase64, dataBase64, librariesBase64 := base64.StdEncoding.EncodeToString(acc.Code), - base64.StdEncoding.EncodeToString(acc.Data) + base64.StdEncoding.EncodeToString(acc.Data), + base64.StdEncoding.EncodeToString(acc.Libraries) - e, err := abi.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, s.bcConfigBase64) + e, err := abi.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, s.bcConfigBase64, librariesBase64) if err != nil { return ret, errors.Wrap(err, "new emulator") } diff --git a/internal/core/account.go b/internal/core/account.go index ee495b1b..78bb8acb 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -74,6 +74,7 @@ type AccountState struct { CodeHash []byte `bun:"type:bytea" json:"code_hash,omitempty"` Data []byte `bun:"type:bytea" json:"data,omitempty"` DataHash []byte `bun:"type:bytea" json:"data_hash,omitempty"` + Libraries []byte `bun:"type:bytea" json:"libraries,omitempty"` GetMethodHashes []int32 `ch:"type:Array(UInt32)" bun:"type:integer[]" json:"get_method_hashes,omitempty"` diff --git a/internal/core/tx.go b/internal/core/tx.go index a4175516..bd1f1562 100644 --- a/internal/core/tx.go +++ b/internal/core/tx.go @@ -59,7 +59,7 @@ func (tx *Transaction) LoadDescription() error { // TODO: optionally load descri return errors.Wrap(err, "load description boc") } - if err := tlb.LoadFromCell(&d, c.BeginParse()); err != nil { + if err := tlb.LoadFromCell(d, c.BeginParse()); err != nil { return errors.Wrap(err, "load description from cell") } diff --git a/migrations/chmigrations/20231007145251_account_state_libraries.down.sql b/migrations/chmigrations/20231007145251_account_state_libraries.down.sql new file mode 100644 index 00000000..fa699cf8 --- /dev/null +++ b/migrations/chmigrations/20231007145251_account_state_libraries.down.sql @@ -0,0 +1,5 @@ +SET statement_timeout = 0; + +--bun:split + +ALTER TABLE account_states DROP COLUMN libraries; diff --git a/migrations/chmigrations/20231007145251_account_state_libraries.up.sql b/migrations/chmigrations/20231007145251_account_state_libraries.up.sql new file mode 100644 index 00000000..d863c4b1 --- /dev/null +++ b/migrations/chmigrations/20231007145251_account_state_libraries.up.sql @@ -0,0 +1 @@ +ALTER TABLE account_states ADD COLUMN libraries String; diff --git a/migrations/pgmigrations/20231007145251_account_state_libraries.down.sql b/migrations/pgmigrations/20231007145251_account_state_libraries.down.sql new file mode 100644 index 00000000..fa699cf8 --- /dev/null +++ b/migrations/pgmigrations/20231007145251_account_state_libraries.down.sql @@ -0,0 +1,5 @@ +SET statement_timeout = 0; + +--bun:split + +ALTER TABLE account_states DROP COLUMN libraries; diff --git a/migrations/pgmigrations/20231007145251_account_state_libraries.up.sql b/migrations/pgmigrations/20231007145251_account_state_libraries.up.sql new file mode 100644 index 00000000..8c8888ed --- /dev/null +++ b/migrations/pgmigrations/20231007145251_account_state_libraries.up.sql @@ -0,0 +1,5 @@ +SET statement_timeout = 0; + +--bun:split + +ALTER TABLE account_states ADD COLUMN libraries bytea; From 546c4e6cb9db563b2d3febb25d254d028f38b236 Mon Sep 17 00:00:00 2001 From: stfy Date: Thu, 12 Oct 2023 21:36:12 +0300 Subject: [PATCH 017/186] fix: Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4a1ecb82..085117b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ COPY abi /go/src/github.com/tonindexer/anton/abi COPY internal /go/src/github.com/tonindexer/anton/internal COPY main.go /go/src/github.com/tonindexer/anton -RUN rm /go/pkg/mod/github.com/tonkeeper/tongo@v1.1.2/lib/linux/libemulator.so +RUN rm /go/pkg/mod/github.com/tonkeeper/tongo@v1.3.0/lib/linux/libemulator.so COPY --from=emulator-builder /output/libemulator.so /lib/libemulator.so RUN swag init \ From f67f0d0bfd180076fd81422508b7b178ae8903b8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 13:57:11 +0530 Subject: [PATCH 018/186] [abi] introduce unions of definitions for messages --- abi/README.md | 156 +++++++++++++++++++++++++--- abi/abi.go | 45 +++++++- abi/get.go | 27 ----- abi/known/getgems_test.go | 2 +- abi/known/telemint_test.go | 4 +- abi/known/tep62_nft_test.go | 2 +- abi/known/tep74_test.go | 4 +- abi/known/tonpay_test.go | 8 +- abi/tlb.go | 97 ++++++++++-------- abi/tlb_test.go | 197 ++++++++++++++++++++++++++++++++++++ abi/tlb_types.go | 3 +- go.mod | 2 +- go.sum | 4 +- 13 files changed, 449 insertions(+), 102 deletions(-) diff --git a/abi/README.md b/abi/README.md index e04cbae7..129d39d6 100644 --- a/abi/README.md +++ b/abi/README.md @@ -82,7 +82,7 @@ Accepted types of `format`: 6. `bigInt` - integer with more than 64 bits, maps into `big.Int` wrapper 7. `cell` - TL-B cell, maps into [`cell.Cell`](https://github.com/xssnick/tonutils-go/blob/4d0157009913e35d450c36e28018cd0686502439/tvm/cell/cell.go#L11) 8. `dict` - TL-B dictionary (hashmap), maps into [`cell.Dictionary`](https://github.com/xssnick/tonutils-go/blob/4d0157009913e35d450c36e28018cd0686502439/tvm/cell/dict.go) -9. `magic` - TL-B constructor prefix, must not be used +9. `tag` - TL-B constructor prefix 10. `coins` - varInt 16, maps into `big.Int` wrapper 11. `addr` - TON address, maps into [`address.Address`](https://github.com/xssnick/tonutils-go/blob/4d0157009913e35d450c36e28018cd0686502439/address/addr.go#L21) wrapper 12. [TODO] `content_cell` - token data as in [TEP-64](https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md); [implementation](https://github.com/xssnick/tonutils-go/blob/b839942a7b7bc431cc610f2ca3d9ff0e03079586/ton/nft/content.go#L10) @@ -163,19 +163,6 @@ Accepted types to map from or into in `format` field: 8. `content` - load [TEP-64](https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md) standard token data into [`nft.ContentAny`](https://github.com/xssnick/tonutils-go/blob/b839942a7b7bc431cc610f2ca3d9ff0e03079586/ton/nft/content.go#L10) 9. `struct` - define struct_fields to parse cell -## Known contracts - -1. TEP-62 NFT Standard: [interfaces](/abi/known/tep62_nft.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0062-nft-standard.md), [contract code](https://github.com/ton-blockchain/token-contract/tree/main/nft) -2. TEP-74 Fungible tokens (Jettons) standard: [interfaces](/abi/known/tep74_jetton.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md), [contract code](https://github.com/ton-blockchain/token-contract/tree/main/ft) -3. TEP-81 DNS contracts: [interface](/abi/known/tep81_dns.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0081-dns-standard.md) -4. TEP-85 NFT SBT tokens: [interfaces](/abi/known/tep85_nft_sbt.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0085-sbt-standard.md) -5. Telemint contracts: [interfaces](/abi/known/telemint.json), [contract code](https://github.com/TelegramMessenger/telemint) -6. Getgems contracts: [interfaces](/abi/known/getgems.json), [contract code](https://github.com/getgems-io/nft-contracts/blob/main/packages/contracts/sources) -7. Wallets: [interfaces](/abi/known/wallets.json), [tonweb](https://github.com/toncenter/tonweb/tree/0a5effd36a3f342f4aacabab728b1f9747085ad1/src/contract/wallet) -8. [STON.fi](https://ston.fi) DEX: [architecture](https://docs.ston.fi/docs/developer-section/architecture), [contract code](https://github.com/ston-fi/dex-core) -9. [Megaton.fi](https://megaton.fi) DEX: [architecture](https://docs.megaton.fi/developers/contract) -10. [Tonpay](https://thetonpay.app): [go-sdk](https://github.com/TheTonpay/tonpay-go-sdk), [js-sdk](https://github.com/TheTonpay/tonpay-js-sdk) - ### Shared TL-B constructors You can define some cell schema in `definitions` field of contract interface. @@ -248,6 +235,147 @@ Or get-method stack values schemas: } ``` +### Union of TLB types + +You can make some definitions with tags in the beginning of cell and use them later in unions. See the following example: + +```json5 +{ + "interface": "jetton_vault", + "definitions": { + "native_asset": [ + { + "name": "native_asset", + "tlb_type": "$0000", + "format": "tag" + } + ], + "jetton_asset": [ + { + "name": "jetton_asset", + "tlb_type": "$0001", + "format": "tag" + }, + // ... + ], + "pool_params": [ + // ... + { + "name": "asset0", + "tlb_type": "[native_asset,jetton_asset]" + }, + // ... + ], + "deposit_liquidity": [ + { + "name": "deposit_liquidity", + "tlb_type": "#40e108d6", + "format": "tag" + }, + { + "name": "pool_params", + "tlb_type": ".", + "format": "pool_params" + }, + // ... + ], + "swap": [ + { + "name": "swap", + "tlb_type": "#e3a0d482", + "format": "tag" + }, + { + "name": "swap_step", + "tlb_type": ".", + "format": "swap_step" + }, + // ... + ] + }, + "in_messages": [ + { + "op_name": "jetton_transfer_notification", + "op_code": "0x7362d09c", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "sender", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "forward_payload", + "tlb_type": "either . ^", + "format": "struct", + "struct_fields": [{ + "name": "value", + "tlb_type": "[deposit_liquidity,swap]" + }] + } + ] + } + ] +} +``` + +Here we define two structs in the interface: `deposit_liquidity` and `swap`. +Then our contract interface accepts incoming `jetton_transfer_notification`. +Inside forward payload there may be a cell, which corresponds to either `deposit_liquidity`, either `swap`. +If Anton finds a message with `jetton_transfer_notification` operation, he will try to determine the structure +of forward payload by tag in the beginning of cell. + +After parsing `deposit_liquidity` transfer notification message body will look like this: + +```json +{ + "query_id": 3638120226682551939, + "amount": "1253854400825677", + "sender": "EQDz0wQL6EEdgbPkFgS7nNmywzr468AvgLyhH7PIMALxPB6G", + "forward_payload": { + "value": { + "deposit_liquidity": {}, + "pool_params": { + "is_stable": false, + "asset_0": { + "native_asset": {} + }, + "asset_1": { + "jetton_asset": {}, + "workchain_id": 0, + "jetton_address": 2422642597 + } + }, + "min_lp_amount": "49289848313582100", + "asset_0_target_balance": "135747634478277169790071850", + "asset_1_target_balance": "30291957672135140790470162860" + } + } +} +``` + +## Known contracts + +1. TEP-62 NFT Standard: [interfaces](/abi/known/tep62_nft.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0062-nft-standard.md), [contract code](https://github.com/ton-blockchain/token-contract/tree/main/nft) +2. TEP-74 Fungible tokens (Jettons) standard: [interfaces](/abi/known/tep74_jetton.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md), [contract code](https://github.com/ton-blockchain/token-contract/tree/main/ft) +3. TEP-81 DNS contracts: [interface](/abi/known/tep81_dns.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0081-dns-standard.md) +4. TEP-85 NFT SBT tokens: [interfaces](/abi/known/tep85_nft_sbt.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0085-sbt-standard.md) +5. Telemint contracts: [interfaces](/abi/known/telemint.json), [contract code](https://github.com/TelegramMessenger/telemint) +6. Getgems contracts: [interfaces](/abi/known/getgems.json), [contract code](https://github.com/getgems-io/nft-contracts/blob/main/packages/contracts/sources) +7. Wallets: [interfaces](/abi/known/wallets.json), [tonweb](https://github.com/toncenter/tonweb/tree/0a5effd36a3f342f4aacabab728b1f9747085ad1/src/contract/wallet) +8. [STON.fi](https://ston.fi) DEX: [architecture](https://docs.ston.fi/docs/developer-section/architecture), [contract code](https://github.com/ston-fi/dex-core) +9. [Megaton.fi](https://megaton.fi) DEX: [architecture](https://docs.megaton.fi/developers/contract) +10. [Tonpay](https://thetonpay.app): [go-sdk](https://github.com/TheTonpay/tonpay-go-sdk), [js-sdk](https://github.com/TheTonpay/tonpay-js-sdk) + ## Converting Golang struct to JSON schema You can convert Golang struct with described tlb tags to the JSON schema by using `abi.NewTLBDesc` and `abi.NewOperationDesc` functions. diff --git a/abi/abi.go b/abi/abi.go index 66a1485d..6f5e28ce 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -1,9 +1,12 @@ package abi import ( + "fmt" "reflect" + "strings" "github.com/pkg/errors" + "github.com/xssnick/tonutils-go/tlb" "github.com/tonindexer/anton/addr" ) @@ -21,14 +24,46 @@ type InterfaceDesc struct { ContractData TLBFieldsDesc `json:"contract_data,omitempty"` } -func (i *InterfaceDesc) RegisterDefinitions() error { - for dn, d := range i.Definitions { - v, err := d.New() +func RegisterDefinitions(definitions map[TLBType]TLBFieldsDesc, depth ...int) error { + noDef := map[TLBType]TLBFieldsDesc{} + for dn, d := range definitions { + dt, err := tlbParseDesc(nil, d) + if err != nil && strings.Contains(err.Error(), "cannot find definition") { + noDef[dn] = d + continue + } if err != nil { return errors.Wrapf(err, "parse '%s' definition", dn) } - typeNameMap[dn] = reflect.TypeOf(v) + + if dt.Field(0).Type == typeNameMap[TLBTag] { + // if the first struct field has tag, + // we register it for the use in unions + tlb.RegisterWithName(string(dn), reflect.New(dt).Elem().Interface()) + } + registeredDefinitions[dn] = d } - return nil + + if len(noDef) == 0 { + return nil + } + + var currentDepth, maxDepth = 0, 16 + if len(depth) > 0 { + currentDepth = depth[0] + } + if len(depth) > 1 { + maxDepth = depth[1] + } + + if currentDepth > maxDepth { + var faultNames []string + for dn := range noDef { + faultNames = append(faultNames, string(dn)) + } + return fmt.Errorf("cannot register [%s] definitions", strings.Join(faultNames, ", ")) + } + + return RegisterDefinitions(noDef, currentDepth+1, maxDepth) } diff --git a/abi/get.go b/abi/get.go index b8492ca8..4c21a31d 100644 --- a/abi/get.go +++ b/abi/get.go @@ -33,33 +33,6 @@ type GetMethodDesc struct { ReturnValues []VmValueDesc `json:"return_values"` } -func (desc *GetMethodDesc) MapRegisteredDefinitions() { - for i := range desc.Arguments { - if desc.Arguments[i].Format == TLBStructCell { - desc.Arguments[i].Fields.MapRegisteredDefinitions() - continue - } - d, ok := registeredDefinitions[desc.Arguments[i].Format] - if ok { - desc.Arguments[i].Format = TLBStructCell - desc.Arguments[i].Fields = d - continue - } - } - for i := range desc.ReturnValues { - if desc.ReturnValues[i].Format == TLBStructCell { - desc.ReturnValues[i].Fields.MapRegisteredDefinitions() - continue - } - d, ok := registeredDefinitions[desc.ReturnValues[i].Format] - if ok { - desc.ReturnValues[i].Format = TLBStructCell - desc.ReturnValues[i].Fields = d - continue - } - } -} - 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/known/getgems_test.go b/abi/known/getgems_test.go index 7cb95811..6f212dba 100644 --- a/abi/known/getgems_test.go +++ b/abi/known/getgems_test.go @@ -29,7 +29,7 @@ func TestGetMethodDesc_GetGemsNFTAuction(t *testing.T) { require.Nil(t, err) for _, i := range interfaces { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) b64, err := base64.StdEncoding.DecodeString(i.CodeBoc) diff --git a/abi/known/telemint_test.go b/abi/known/telemint_test.go index 41bd56b5..b83c3dd8 100644 --- a/abi/known/telemint_test.go +++ b/abi/known/telemint_test.go @@ -160,7 +160,7 @@ func TestOperationDesc_TelemintNFTCollection(t *testing.T) { for _, i = range interfaces { if i.Name == "telemint_nft_collection" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } @@ -209,7 +209,7 @@ func TestGetMethodDesc_TelemintNFTItem(t *testing.T) { for _, i = range interfaces { if i.Name == "telemint_nft_item" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } diff --git a/abi/known/tep62_nft_test.go b/abi/known/tep62_nft_test.go index 3eecfc5e..93ee9d01 100644 --- a/abi/known/tep62_nft_test.go +++ b/abi/known/tep62_nft_test.go @@ -99,7 +99,7 @@ func TestLoadOperation_NFTCollection(t *testing.T) { // tx hash 19a40062e31365d6ad4473aabb62562f37d04c8aa5618b7ea800885dbb5a0e70 schema: `{"op_name":"nft_collection_change_content","op_code":"0x4","body":[{"name":"query_id","tlb_type":"## 64","format":"uint64"},{"name":"content","tlb_type":"^","format":"cell"}]}`, boc: `te6cckEBBQEAxQACGAAAAAQAAAAAAAAAAAIBAEsAAABkgAe4JggE07o9i50C5vJ2aQiIbUYwl8/YMW27aEtS1YEfMAIABAMAaGh0dHBzOi8vcy5nZXRnZW1zLmlvL25mdC9jLzYzYTgwMDVlY2MxM2M0OTE0YjMxNGIyMy8AogFodHRwczovL3MuZ2V0Z2Vtcy5pby9uZnQvYy82M2E4MDA1ZWNjMTNjNDkxNGIzMTRiMjMvZWRpdC9tZXRhLTE2NzIyODIyMDQ1ODkuanNvbml2vhQ=`, - expected: `{"query_id":0,"content":"te6cckEBAwEAjQACAAIBAGhodHRwczovL3MuZ2V0Z2Vtcy5pby9uZnQvYy82M2E4MDA1ZWNjMTNjNDkxNGIzMTRiMjMvAKIBaHR0cHM6Ly9zLmdldGdlbXMuaW8vbmZ0L2MvNjNhODAwNWVjYzEzYzQ5MTRiMzE0YjIzL2VkaXQvbWV0YS0xNjcyMjgyMjA0NTg5Lmpzb279fEK3"}`, + expected: `{"query_id":0,"content":"te6cckEBAwEAjQACAAECAKIBaHR0cHM6Ly9zLmdldGdlbXMuaW8vbmZ0L2MvNjNhODAwNWVjYzEzYzQ5MTRiMzE0YjIzL2VkaXQvbWV0YS0xNjcyMjgyMjA0NTg5Lmpzb24AaGh0dHBzOi8vcy5nZXRnZW1zLmlvL25mdC9jLzYzYTgwMDVlY2MxM2M0OTE0YjMxNGIyMy+TM8WI"}`, }, } diff --git a/abi/known/tep74_test.go b/abi/known/tep74_test.go index a03d3927..7bfa1974 100644 --- a/abi/known/tep74_test.go +++ b/abi/known/tep74_test.go @@ -155,7 +155,7 @@ func TestOperationDesc_JettonMinter(t *testing.T) { for _, i = range interfaces { if i.Name == "jetton_minter" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } @@ -197,7 +197,7 @@ func TestOperationDesc_JettonWallet_JettonBurn_Optional(t *testing.T) { for _, i = range interfaces { if i.Name == "jetton_wallet" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } diff --git a/abi/known/tonpay_test.go b/abi/known/tonpay_test.go index 22b037fd..769e3cec 100644 --- a/abi/known/tonpay_test.go +++ b/abi/known/tonpay_test.go @@ -29,7 +29,7 @@ func TestOperationDesc_TonpayStore(t *testing.T) { for _, i = range interfaces { if i.Name == "tonpay_store" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } @@ -95,7 +95,7 @@ func TestGetMethodDesc_TonpayStore(t *testing.T) { for _, i = range interfaces { if i.Name == "tonpay_store" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } @@ -177,7 +177,7 @@ func TestOperationDesc_TonpayInvoice(t *testing.T) { for _, i = range interfaces { if i.Name == "tonpay_invoice" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } @@ -216,7 +216,7 @@ func TestGetMethodDesc_TonpayInvoice(t *testing.T) { for _, i = range interfaces { if i.Name == "tonpay_invoice" { - err := i.RegisterDefinitions() + err := abi.RegisterDefinitions(i.Definitions) require.Nil(t, err) break } diff --git a/abi/tlb.go b/abi/tlb.go index 8dabf9e0..78f1a68a 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -32,7 +32,7 @@ type OperationDesc struct { Body TLBFieldsDesc `json:"body"` } -func tlbMakeDesc(t reflect.Type) (ret TLBFieldsDesc, err error) { +func tlbMakeDesc(t reflect.Type, skipMagic ...bool) (ret TLBFieldsDesc, err error) { for i := 0; i < t.NumField(); i++ { f := t.Field(i) @@ -41,7 +41,7 @@ func tlbMakeDesc(t reflect.Type) (ret TLBFieldsDesc, err error) { Type: f.Tag.Get("tlb"), } - if i == 0 && f.Type == reflect.TypeOf(tlb.Magic{}) { + if len(skipMagic) > 0 && skipMagic[0] && i == 0 && f.Type == reflect.TypeOf(tlb.Magic{}) { continue // skip tlb constructor tag as it has to be inside OperationDesc } @@ -139,6 +139,19 @@ func tlbParseSettings(tag string) (reflect.Type, error) { return nil, nil } + if strings.HasPrefix(settings[0], "[") && strings.HasSuffix(settings[0], "]") { + for _, dn := range strings.Split(tag[1:len(tag)-1], ",") { + // iterate through union definitions + // check that all definitions are known + _, ok := registeredDefinitions[TLBType(dn)] + if !ok { + return nil, fmt.Errorf("cannot find definition for '%s' type inside union", dn) + } + } + var v any + return reflect.ValueOf(&v).Type().Elem(), nil // return type with interface kind + } + switch settings[0] { case "maybe": return reflect.TypeOf((*cell.Cell)(nil)), nil @@ -172,11 +185,36 @@ func tlbParseSettings(tag string) (reflect.Type, error) { } } +func tlbMapFormat(format TLBType, tag reflect.StructTag) (reflect.Type, error) { + t, ok := typeNameMap[format] + if ok { + return t, nil + } + + switch format { + case "": + // parse tlb tag and get default type + t, err := tlbParseSettings(tag.Get("tlb")) + if t == nil || err != nil { + return nil, fmt.Errorf("parse tlb settings with tag %s: %w", tag.Get("tlb"), err) + } + return t, nil + + default: + d, ok := registeredDefinitions[format] + if !ok { + return nil, fmt.Errorf("cannot find definition for '%s' format", format) + } + t, err := tlbParseDesc(nil, d) + if err != nil { + return nil, errors.Wrap(err, "creating new type from definition") + } + return reflect.PointerTo(t), nil + } +} + func tlbParseDesc(fields []reflect.StructField, schema TLBFieldsDesc, skipOptional ...bool) (reflect.Type, error) { - var ( - err error - ok bool - ) + var err error for i := range schema { f := &schema[i] @@ -184,32 +222,26 @@ func tlbParseDesc(fields []reflect.StructField, schema TLBFieldsDesc, skipOption if len(skipOptional) > 0 && skipOptional[0] && f.Optional { continue } + if len(f.Fields) > 0 && f.Format == "" { + f.Format = TLBStructCell + } var sf = reflect.StructField{ Name: strcase.ToCamel(f.Name), Tag: reflect.StructTag(fmt.Sprintf("tlb:%q json:%q", f.Type, strcase.ToSnake(f.Name))), } - // get type from `format` field - sf.Type, ok = typeNameMap[f.Format] - if !ok { - if f.Format != "" && f.Format != TLBStructCell { - return nil, fmt.Errorf("unknown format '%s'", f.Format) - } - // parse tlb tag and get default type - sf.Type, err = tlbParseSettings(sf.Tag.Get("tlb")) - if sf.Type == nil || err != nil { - return nil, fmt.Errorf("%s (tag = %s) parse tlb settings: %w", sf.Name, sf.Tag.Get("tlb"), err) - } - } - - // make new struct - if len(f.Fields) > 0 { + if f.Format == TLBStructCell { sf.Type, err = tlbParseDesc(nil, f.Fields, skipOptional...) if err != nil { - return nil, fmt.Errorf("%s: %w", sf.Name, err) + return nil, fmt.Errorf("%s field with struct: %w", sf.Name, err) } sf.Type = reflect.PointerTo(sf.Type) + } else { + sf.Type, err = tlbMapFormat(f.Format, sf.Tag) + if err != nil { + return nil, errors.Wrapf(err, "%s field", f.Name) + } } fields = append(fields, sf) @@ -234,21 +266,6 @@ func (desc TLBFieldsDesc) New(skipOptional ...bool) (any, error) { return reflect.New(t).Interface(), nil } -func (desc TLBFieldsDesc) MapRegisteredDefinitions() { - for i := range desc { - if desc[i].Format == TLBStructCell { - desc[i].Fields.MapRegisteredDefinitions() - continue - } - d, ok := registeredDefinitions[desc[i].Format] - if ok { - desc[i].Format = TLBStructCell - desc[i].Fields = d - continue - } - } -} - func (desc TLBFieldsDesc) FromCell(c *cell.Cell) (any, error) { parsed, err := desc.New() if err != nil { @@ -310,7 +327,7 @@ func NewOperationDesc(x any) (*OperationDesc, error) { } ret.Code = fmt.Sprintf("0x%x", opCode) - ret.Body, err = tlbMakeDesc(t) + ret.Body, err = tlbMakeDesc(t, true) if err != nil { return nil, errors.Wrap(err, "make tlb schema") } @@ -333,10 +350,6 @@ func (desc *OperationDesc) New(skipOptional ...bool) (any, error) { return reflect.New(t).Interface(), nil } -func (desc *OperationDesc) MapRegisteredDefinitions() { - desc.Body.MapRegisteredDefinitions() -} - func (desc *OperationDesc) FromCell(c *cell.Cell) (any, error) { parsed, err := desc.New() if err != nil { diff --git a/abi/tlb_test.go b/abi/tlb_test.go index d3b1244d..f2b7ba62 100644 --- a/abi/tlb_test.go +++ b/abi/tlb_test.go @@ -1,6 +1,7 @@ package abi_test import ( + "encoding/base64" "encoding/json" "math/big" "testing" @@ -110,3 +111,199 @@ func TestTLBFieldsDesc_LoadFromCell(t *testing.T) { exp := `{"small_int":42,"big_int":8000000000000000000000000,"ref_struct":{"addr":"EQDj5AA8mQvM5wJEQsFFFof79y3ZsuX6wowktWQFhz_Anton"},"embed_struct":{"bits":"YXNkZg=="},"maybe_cell":null,"either_cell":"te6cckEBAQEACAAADGVpdGhlcskJ1lc="}` require.Equal(t, exp, string(j)) } + +func TestTLBFieldsDesc_LoadFromCell_DefinitionsUnion(t *testing.T) { + j := []byte(`{ + "interface": "jetton_vault", + "definitions": { + "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": "## 32", + "format": "uint32" + } + ], + "pool_params": [ + { + "name": "is_stable", + "tlb_type": "bool" + }, + { + "name": "asset0", + "tlb_type": "[native_asset,jetton_asset]" + }, + { + "name": "asset1", + "tlb_type": "[native_asset,jetton_asset]" + } + ], + "deposit_liquidity": [ + { + "name": "deposit_liquidity", + "tlb_type": "#40e108d6", + "format": "tag" + }, + { + "name": "pool_params", + "tlb_type": ".", + "format": "pool_params" + }, + { + "name": "min_lp_amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "asset0_target_balance", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "asset1_target_balance", + "tlb_type": ".", + "format": "coins" + } + ], + "swap_step_params": [ + { + "name": "swap_kind", + "tlb_type": "## 1", + "format": "uint8" + }, + { + "name": "limit", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "next", + "tlb_type": "maybe ^", + "format": "cell" + } + ], + "swap_step": [ + { + "name": "pool_addr", + "tlb_type": "addr" + }, + { + "name": "params", + "tlb_type": ".", + "format": "swap_step_params" + } + ], + "swap_params": [ + { + "name": "deadline", + "tlb_type": "## 32", + "format": "uint32" + }, + { + "name": "recipient_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "referral_addr", + "tlb_type": "addr", + "format": "addr" + } + ], + "swap": [ + { + "name": "swap", + "tlb_type": "#e3a0d482", + "format": "tag" + }, + { + "name": "swap_step", + "tlb_type": ".", + "format": "swap_step" + }, + { + "name": "swap_params", + "tlb_type": "^", + "format": "swap_params" + } + ] + }, + "in_messages": [ + { + "op_name": "jetton_transfer_notification", + "op_code": "0x7362d09c", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "sender", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "forward_payload", + "tlb_type": "either . ^", + "format": "struct", + "struct_fields": [ + { + "name": "value", + "tlb_type": "[deposit_liquidity,swap]" + } + ] + } + ] + } + ] +}`) + + var i abi.InterfaceDesc + + err := json.Unmarshal(j, &i) + require.Nil(t, err) + + err = abi.RegisterDefinitions(i.Definitions) + require.Nil(t, err) + + // body, err := base64.StdEncoding.DecodeString(`te6cckEBAwEAaAABaHNi0JwAAAJovkRKsWAQCc9A8DgB1SCJzWjksBzMruGpmYclZnRmWWc2C4h83mgaryg4Sd8BAU3joNSCgB8qWTCcZtGRrvci/dxNr39DgHo5VaQVpPrkLnQ6xUf9YEACAAkAAAAAAshothE=`) + // require.Nil(t, err) + body, err := base64.StdEncoding.DecodeString(`te6cckEBAgEAbQABanNi0JwyfTMSEZO+g3BHRfuilJTYAeemCBfQgjsDZ8gsCXc5s2WGdfHXgF8BeUI/Z5BgBeJ5AQBlQOEI1gCASDNL0r145tjeHJCluCTXAVnklS2GGhVjDwdcXsmCWviCHc1lADgjov3RSkppLrJXaA==`) + require.Nil(t, err) + + c, err := cell.FromBOC(body) + require.Nil(t, err) + + got, err := i.InMessages[0].FromCell(c) + require.Nil(t, err) + + j, err = json.Marshal(got) + require.Nil(t, err) + + require.Equal(t, + // `{"query_id":2648892000945,"amount":"1102144868099","sender":"EQDqkETmtHJYDmZXcNTMw5KzOjMss5sFxD5vNA1XlBwk7_mo","forward_payload":{"value":{"swap":{},"swap_step":{"pool_addr":"EQD5UsmE4zaMjXe5F-7ibXv6HAPRyq0grSfXIXOh1io_6xmV","params":{"swap_kind":0,"limit":"0","next":null}},"swap_params":{"deadline":0,"recipient_addr":"NONE","referral_addr":"NONE"}}}}`, + `{"query_id":3638120226682551939,"amount":"1253854400825677","sender":"EQDz0wQL6EEdgbPkFgS7nNmywzr468AvgLyhH7PIMALxPB6G","forward_payload":{"value":{"deposit_liquidity":{},"pool_params":{"is_stable":false,"asset_0":{"native_asset":{}},"asset_1":{"jetton_asset":{},"workchain_id":0,"jetton_address":2422642597}},"min_lp_amount":"49289848313582100","asset_0_target_balance":"135747634478277169790071850","asset_1_target_balance":"30291957672135140790470162860"}}}`, + string(j)) +} diff --git a/abi/tlb_types.go b/abi/tlb_types.go index 2fe47b79..810aee9c 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -22,6 +22,7 @@ const ( TLBCell TLBType = "cell" TLBContentCell TLBType = "content" TLBStructCell TLBType = "struct" + TLBTag TLBType = "tag" ) type TelemintText struct { @@ -75,7 +76,7 @@ var ( TLBBigInt: reflect.TypeOf(big.NewInt(0)), TLBCell: reflect.TypeOf((*cell.Cell)(nil)), "dict": reflect.TypeOf((*cell.Dictionary)(nil)), - "magic": reflect.TypeOf(tlb.Magic{}), + TLBTag: reflect.TypeOf(tlb.Magic{}), "coins": reflect.TypeOf(tlb.Coins{}), TLBAddr: reflect.TypeOf((*address.Address)(nil)), TLBString: reflect.TypeOf((*StringSnake)(nil)), diff --git a/go.mod b/go.mod index d854f7b5..ed9c9121 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.8.4 + github.com/xssnick/tonutils-go v1.8.5-0.20231013083545-4f085f4fc80f ) require github.com/gin-contrib/cors v1.4.0 diff --git a/go.sum b/go.sum index 648d64db..0d526537 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,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.8.4 h1:RdMau+dNRMC/N4IvceFKnOK0xLYwxf4EuYf/rzGYVvY= -github.com/xssnick/tonutils-go v1.8.4/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= +github.com/xssnick/tonutils-go v1.8.5-0.20231013083545-4f085f4fc80f h1:0wxnLLvfDe0soBkITECdT9D2DmcBMIe9Xzv3+gi0W6o= +github.com/xssnick/tonutils-go v1.8.5-0.20231013083545-4f085f4fc80f/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= From 84f9ba534da6f8cabf24d910d23f1c47835ee453 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 14:26:21 +0530 Subject: [PATCH 019/186] [cmd] save definitions to the database and register them on indexer start --- cmd/contract/interface.go | 36 ++++++++++------ cmd/indexer/indexer.go | 16 ++++++- cmd/web/web.go | 11 +++++ internal/app/parser/parser_test.go | 6 +++ internal/core/contract.go | 13 ++++-- internal/core/repository/contract/contract.go | 39 +++++++++++++++++ .../core/repository/contract/contract_test.go | 42 ++++++++++++++++++- 7 files changed, 145 insertions(+), 18 deletions(-) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index a607959f..d1ff9a37 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -13,6 +13,7 @@ import ( "github.com/allisson/go-env" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" @@ -75,8 +76,6 @@ func parseOperationDesc(t abi.ContractName, d *abi.OperationDesc) (*core.Contrac opId = uint32(n) } - d.MapRegisteredDefinitions() - // this is needed to map interface definitions into schema x, err := d.New() if err != nil { @@ -116,7 +115,6 @@ func parseInterfaceDesc(d *abi.InterfaceDesc) (*core.ContractInterface, []*core. GetMethodsDesc: d.GetMethods, } for it := range i.GetMethodsDesc { - i.GetMethodsDesc[it].MapRegisteredDefinitions() i.GetMethodHashes = append(i.GetMethodHashes, abi.MethodNameHash(i.GetMethodsDesc[it].Name)) } if len(i.Code) == 0 { @@ -144,15 +142,21 @@ func parseInterfaceDesc(d *abi.InterfaceDesc) (*core.ContractInterface, []*core. return &i, operations, nil } -func parseInterfacesDesc(descriptors []*abi.InterfaceDesc) (retI []*core.ContractInterface, retOp []*core.ContractOperation, _ error) { - for _, d := range descriptors { - err := d.RegisterDefinitions() +func parseInterfacesDesc(descriptors []*abi.InterfaceDesc) (retD map[abi.TLBType]abi.TLBFieldsDesc, retI []*core.ContractInterface, retOp []*core.ContractOperation, _ error) { + retD = map[abi.TLBType]abi.TLBFieldsDesc{} + for _, desc := range descriptors { + err := abi.RegisterDefinitions(desc.Definitions) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - i, operations, err := parseInterfaceDesc(d) + for dn, d := range desc.Definitions { + retD[dn] = d + } + } + for _, desc := range descriptors { + i, operations, err := parseInterfaceDesc(desc) if err != nil { - return nil, nil, err + return nil, nil, nil, err } retI = append(retI, i) retOp = append(retOp, operations...) @@ -224,7 +228,7 @@ var Command = &cli.Command{ return err } - interfaces, operations, err := parseInterfacesDesc(interfacesDesc) + definitions, interfaces, operations, err := parseInterfacesDesc(interfacesDesc) if err != nil { return err } @@ -241,14 +245,22 @@ var Command = &cli.Command{ return errors.Wrapf(err, "cannot ping postgresql") } + for dn, d := range definitions { + if err := contract.NewRepository(pg).AddDefinition(ctx.Context, dn, d); err != nil { + log.Err(err).Str("definition_name", string(dn)).Msg("cannot insert contract interface") + } + } for _, i := range interfaces { if err := contract.NewRepository(pg).AddInterface(ctx.Context, i); err != nil { - return errors.Wrapf(err, "cannot insert %s contract interface", i.Name) + log.Err(err).Str("interface_name", string(i.Name)).Msg("cannot insert contract interface") } } for _, op := range operations { if err := contract.NewRepository(pg).AddOperation(ctx.Context, op); err != nil { - return errors.Wrapf(err, "cannot insert %s %s contract operation", op.ContractName, op.OperationName) + log.Err(err). + Str("interface_name", string(op.ContractName)). + Str("operation_name", op.OperationName). + Msg("cannot insert contract operation") } } diff --git a/cmd/indexer/indexer.go b/cmd/indexer/indexer.go index 1a352ccc..e3000e8b 100644 --- a/cmd/indexer/indexer.go +++ b/cmd/indexer/indexer.go @@ -13,6 +13,7 @@ import ( "github.com/xssnick/tonutils-go/liteclient" "github.com/xssnick/tonutils-go/ton" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/app/fetcher" "github.com/tonindexer/anton/internal/app/indexer" @@ -35,7 +36,9 @@ var Command = &cli.Command{ return errors.Wrap(err, "cannot connect to a database") } - interfaces, err := contract.NewRepository(conn.PG).GetInterfaces(ctx.Context) + contractRepo := contract.NewRepository(conn.PG) + + interfaces, err := contractRepo.GetInterfaces(ctx.Context) if err != nil { return errors.Wrap(err, "get interfaces") } @@ -43,6 +46,15 @@ var Command = &cli.Command{ return errors.New("no contract interfaces") } + def, err := contractRepo.GetDefinitions(ctx.Context) + if err != nil { + return errors.Wrap(err, "get definitions") + } + err = abi.RegisterDefinitions(def) + if err != nil { + return errors.Wrap(err, "get definitions") + } + client := liteclient.NewConnectionPool() api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { @@ -62,7 +74,7 @@ var Command = &cli.Command{ p := parser.NewService(&app.ParserConfig{ BlockchainConfig: bcConfig, - ContractRepo: contract.NewRepository(conn.PG), + ContractRepo: contractRepo, }) f := fetcher.NewService(&app.FetcherConfig{ API: api, diff --git a/cmd/web/web.go b/cmd/web/web.go index 2632ddee..9ae240e4 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -13,10 +13,12 @@ import ( "github.com/xssnick/tonutils-go/liteclient" "github.com/xssnick/tonutils-go/ton" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/api/http" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/app/query" "github.com/tonindexer/anton/internal/core/repository" + "github.com/tonindexer/anton/internal/core/repository/contract" ) var Command = &cli.Command{ @@ -33,6 +35,15 @@ var Command = &cli.Command{ return errors.Wrap(err, "cannot connect to a database") } + def, err := contract.NewRepository(conn.PG).GetDefinitions(ctx.Context) + if err != nil { + return errors.Wrap(err, "get definitions") + } + err = abi.RegisterDefinitions(def) + if err != nil { + return errors.Wrap(err, "get definitions") + } + client := liteclient.NewConnectionPool() api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index 90459d1f..30403ed3 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -34,6 +34,12 @@ type mockContractRepo struct { interfaces []*core.ContractInterface } +func (m *mockContractRepo) AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error { + panic("implement me") +} +func (m *mockContractRepo) GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { + panic("implement me") +} func (m *mockContractRepo) AddInterface(_ context.Context, _ *core.ContractInterface) error { panic("implement me") } diff --git a/internal/core/contract.go b/internal/core/contract.go index acb1ba5b..87d332f1 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -9,6 +9,13 @@ import ( "github.com/tonindexer/anton/addr" ) +type ContractDefinition struct { + bun.BaseModel `bun:"table:contract_definitions" json:"-"` + + Name abi.TLBType `bun:",pk" json:"name"` + Schema abi.TLBFieldsDesc `json:"schema"` +} + type ContractInterface struct { bun.BaseModel `bun:"table:contract_interfaces" json:"-"` @@ -32,15 +39,15 @@ type ContractOperation struct { } type ContractRepository interface { - AddInterface(context.Context, *ContractInterface) error + AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error + GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) + AddInterface(context.Context, *ContractInterface) error DelInterface(ctx context.Context, name string) error - GetInterfaces(context.Context) ([]*ContractInterface, error) GetMethodDescription(ctx context.Context, name abi.ContractName, method string) (abi.GetMethodDesc, error) AddOperation(context.Context, *ContractOperation) error - GetOperations(context.Context) ([]*ContractOperation, error) GetOperationByID(context.Context, MessageType, []abi.ContractName, bool, uint32) (*ContractOperation, error) } diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 8cf0134d..92bcf1a7 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -25,6 +25,15 @@ func NewRepository(db *bun.DB) *Repository { func CreateTables(ctx context.Context, pgDB *bun.DB) error { _, err := pgDB.NewCreateTable(). + Model(&core.ContractDefinition{}). + IfNotExists(). + WithForeignKeys(). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "contract definitions pg create table") + } + + _, err = pgDB.NewCreateTable(). Model(&core.ContractOperation{}). IfNotExists(). WithForeignKeys(). @@ -60,6 +69,36 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return nil } +func (r *Repository) AddDefinition(ctx context.Context, dn abi.TLBType, d abi.TLBFieldsDesc) error { + def := &core.ContractDefinition{ + Name: dn, + Schema: d, + } + + _, err := r.pg.NewInsert().Model(def).Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func (r *Repository) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { + var ret []*core.ContractDefinition + + err := r.pg.NewSelect().Model(&ret).Scan(ctx) + if err != nil { + return nil, err + } + + res := map[abi.TLBType]abi.TLBFieldsDesc{} + for _, def := range ret { + res[def.Name] = def.Schema + } + + return res, nil +} + func (r *Repository) AddInterface(ctx context.Context, i *core.ContractInterface) error { _, err := r.pg.NewInsert().Model(i).Exec(ctx) if err != nil { diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 6f52d1ef..45ab8f02 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -54,6 +54,8 @@ func dropTables(t testing.TB) { require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractInterface)(nil)).IfExists().Exec(ctx) require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.ContractDefinition)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) _, err = pg.ExecContext(context.Background(), "DROP TYPE message_type") if err != nil && !strings.Contains(err.Error(), "does not exist") { @@ -64,6 +66,32 @@ func dropTables(t testing.TB) { func TestRepository_AddContracts(t *testing.T) { initdb(t) + d := core.ContractDefinition{ + Name: "test_definition", + } + + definitionSchema := []byte(`[ + { + "name": "new_owner", + "tlb_type": "addr" + }, + { + "name": "response_destination", + "tlb_type": "addr" + }, + { + "name": "custom_payload", + "tlb_type": "maybe ^" + }, + { + "name": "forward_amount", + "tlb_type": ".", + "format": "coins" + } +]`) + err := json.Unmarshal(definitionSchema, &d.Schema) + require.Nil(t, err) + i := &core.ContractInterface{ Name: known.NFTItem, Addresses: []*addr.Address{rndm.Address()}, @@ -126,7 +154,7 @@ func TestRepository_AddContracts(t *testing.T) { }` var opSchema abi.OperationDesc - err := json.Unmarshal([]byte(schema), &opSchema) + err = json.Unmarshal([]byte(schema), &opSchema) require.Nil(t, err) op := &core.ContractOperation{ @@ -149,6 +177,18 @@ func TestRepository_AddContracts(t *testing.T) { createTables(t) }) + t.Run("insert definition", func(t *testing.T) { + err := repo.AddDefinition(ctx, d.Name, d.Schema) + require.Nil(t, err) + }) + + t.Run("get definitions", func(t *testing.T) { + m, err := repo.GetDefinitions(ctx) + require.Nil(t, err) + require.Equal(t, 1, len(m)) + require.Equal(t, m[d.Name], d.Schema) + }) + t.Run("insert interface", func(t *testing.T) { err := repo.AddInterface(ctx, i) require.Nil(t, err) From e94715236fce8003b88ceeeeec36a98e1be05ecd Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 14:49:07 +0530 Subject: [PATCH 020/186] [migrations] create contract definitions --- .../20231014090410_contract_definitions_table.down.sql | 5 +++++ .../20231014090410_contract_definitions_table.up.sql | 5 +++++ .../20231014090410_contract_definitions_table.down.sql | 9 +++++++++ .../20231014090410_contract_definitions_table.up.sql | 7 +++++++ 4 files changed, 26 insertions(+) create mode 100644 migrations/chmigrations/20231014090410_contract_definitions_table.down.sql create mode 100644 migrations/chmigrations/20231014090410_contract_definitions_table.up.sql create mode 100644 migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql create mode 100644 migrations/pgmigrations/20231014090410_contract_definitions_table.up.sql diff --git a/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql b/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql new file mode 100644 index 00000000..d4906491 --- /dev/null +++ b/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql @@ -0,0 +1,5 @@ +SELECT 1 + +--migration:split + +SELECT 2 diff --git a/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql b/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql new file mode 100644 index 00000000..d4906491 --- /dev/null +++ b/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql @@ -0,0 +1,5 @@ +SELECT 1 + +--migration:split + +SELECT 2 diff --git a/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql b/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql new file mode 100644 index 00000000..87d60f3e --- /dev/null +++ b/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql @@ -0,0 +1,9 @@ +SET statement_timeout = 0; + +--bun:split + +SELECT 1 + +--bun:split + +SELECT 2 diff --git a/migrations/pgmigrations/20231014090410_contract_definitions_table.up.sql b/migrations/pgmigrations/20231014090410_contract_definitions_table.up.sql new file mode 100644 index 00000000..8cc37d31 --- /dev/null +++ b/migrations/pgmigrations/20231014090410_contract_definitions_table.up.sql @@ -0,0 +1,7 @@ +SET statement_timeout = 0; + +CREATE TABLE contract_definitions ( + name text NOT NULL, + schema jsonb NOT NULL, + CONSTRAINT contract_definitions_pkey PRIMARY KEY (name) +); From f46444973360c71a44cf23ec0699c8b62af8aecb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 14:49:51 +0530 Subject: [PATCH 021/186] [abi] get-emulator: support struct --- abi/get_emulator.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index b15f7630..2d510043 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -307,6 +307,10 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return nil, errors.Wrap(err, "convert boc to cell") } + if d.Format == "" && len(d.Fields) > 0 { + d.Format = TLBStructCell + } + switch d.Format { case "", TLBCell: return c, nil @@ -369,6 +373,10 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return nil, errors.Wrap(err, "convert boc to cell") } + if d.Format == "" && len(d.Fields) > 0 { + d.Format = TLBStructCell + } + switch d.Format { case "": return c.BeginParse(), nil @@ -380,6 +388,12 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return nil, errors.Wrap(err, "load address") } return a, nil + case TLBStructCell: + parsed, err := d.Fields.FromCell(c) + if err != nil { + return nil, errors.Wrapf(err, "load struct from slice on %s value description schema", d.Name) + } + return parsed, nil default: return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) } From 65b464dc8499658a4631e664616eedbc5ba028d6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 14:50:39 +0530 Subject: [PATCH 022/186] [abi] tlbMakeDesc: skip format determination on union --- abi/tlb.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/abi/tlb.go b/abi/tlb.go index 78f1a68a..31ae1e7b 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -64,6 +64,9 @@ func tlbMakeDesc(t reflect.Type, skipMagic ...bool) (ret TLBFieldsDesc, err erro return nil, fmt.Errorf("%s: %w", f.Name, err) } + case strings.HasPrefix(schema.Type, "[") && strings.HasSuffix(schema.Type, "]"): + // no format for union + default: return nil, fmt.Errorf("%s: unknown structField type %s", f.Name, f.Type) } From d0e5d9cb9adfc0797479eea7b301f09ac1868a30 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 14:51:02 +0530 Subject: [PATCH 023/186] [core] update pg types of contract definitions --- internal/core/contract.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/contract.go b/internal/core/contract.go index 87d332f1..e4ced4fb 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -12,8 +12,8 @@ import ( type ContractDefinition struct { bun.BaseModel `bun:"table:contract_definitions" json:"-"` - Name abi.TLBType `bun:",pk" json:"name"` - Schema abi.TLBFieldsDesc `json:"schema"` + Name abi.TLBType `bun:",pk,notnull" json:"name"` + Schema abi.TLBFieldsDesc `bun:"type:jsonb,notnull" json:"schema"` } type ContractInterface struct { From d92b6d013d583b176b8adea949bea19d3d10de9f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 15:36:46 +0530 Subject: [PATCH 024/186] [abi] get-methods return values parsing: accept definitions --- abi/get_emulator.go | 56 +++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 2d510043..f5b2afc8 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -277,10 +277,10 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } } -func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { +func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": - switch d.Format { + switch desc.Format { case "", TLBCell, TLBStructCell: return (*cell.Cell)(nil), nil case TLBString: @@ -288,14 +288,14 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { case TLBContentCell: return nft.ContentAny(nil), nil default: - return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) + return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) } case "VmStkCell": // go further default: - return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", d.StackType, v.SumType) + return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", desc.StackType, v.SumType) } tgcBoc, err := v.VmStkCell.Value.ToBocCustom(false, false, false, 0) @@ -307,11 +307,11 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return nil, errors.Wrap(err, "convert boc to cell") } - if d.Format == "" && len(d.Fields) > 0 { - d.Format = TLBStructCell + if desc.Format == "" && len(desc.Fields) > 0 { + desc.Format = TLBStructCell } - switch d.Format { + switch desc.Format { case "", TLBCell: return c, nil case TLBString: @@ -333,20 +333,28 @@ func vmParseValueCell(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } return content, nil case TLBStructCell: - parsed, err := d.Fields.FromCell(c) + parsed, err := desc.Fields.FromCell(c) if err != nil { - return nil, errors.Wrapf(err, "load struct from cell on %s value description schema", d.Name) + return nil, errors.Wrapf(err, "load struct from cell on %s value description schema", desc.Name) } return parsed, nil default: - return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) + d, ok := registeredDefinitions[desc.Format] + if !ok { + return nil, fmt.Errorf("cannot find definition for '%s' format", desc.Format) + } + parsed, err := d.FromCell(c) + if err != nil { + return nil, errors.Wrapf(err, "'%s' definition from cell", desc.Format) + } + return parsed, nil } } -func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { +func vmParseValueSlice(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": - switch d.Format { + switch desc.Format { case "": return (*cell.Slice)(nil), nil case TLBAddr: @@ -354,14 +362,14 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { case TLBString: return "", nil default: - return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) + return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) } case "VmStkSlice": // go further default: - return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", d.StackType, v.SumType) + return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", desc.StackType, v.SumType) } tgcBoc, err := v.VmStkSlice.Cell().ToBocCustom(false, false, false, 0) @@ -373,11 +381,11 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { return nil, errors.Wrap(err, "convert boc to cell") } - if d.Format == "" && len(d.Fields) > 0 { - d.Format = TLBStructCell + if desc.Format == "" && len(desc.Fields) > 0 { + desc.Format = TLBStructCell } - switch d.Format { + switch desc.Format { case "": return c.BeginParse(), nil case TLBString: @@ -389,13 +397,21 @@ func vmParseValueSlice(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } return a, nil case TLBStructCell: - parsed, err := d.Fields.FromCell(c) + parsed, err := desc.Fields.FromCell(c) if err != nil { - return nil, errors.Wrapf(err, "load struct from slice on %s value description schema", d.Name) + return nil, errors.Wrapf(err, "load struct from slice on %s value description schema", desc.Name) } return parsed, nil default: - return nil, fmt.Errorf("unsupported '%s' format for '%s' type", d.Format, d.StackType) + d, ok := registeredDefinitions[desc.Format] + if !ok { + return nil, fmt.Errorf("cannot find definition for '%s' format", desc.Format) + } + parsed, err := d.FromCell(c) + if err != nil { + return nil, errors.Wrapf(err, "'%s' definition from cell", desc.Format) + } + return parsed, nil } } From d39a61c16d5dbac346178f21b51ebc84adfdb053 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 15:43:42 +0530 Subject: [PATCH 025/186] [abi] prettify get-method emulator cell parsing --- abi/get_emulator.go | 115 ++++++++++++++++++++------------------------ abi/tlb_types.go | 1 + 2 files changed, 53 insertions(+), 63 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index f5b2afc8..461b74ad 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -277,67 +277,42 @@ func vmParseValueInt(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { } } -func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { - switch v.SumType { - case "VmStkNull": - switch desc.Format { - case "", TLBCell, TLBStructCell: - return (*cell.Cell)(nil), nil - case TLBString: - return "", nil - case TLBContentCell: - return nft.ContentAny(nil), nil - default: - return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) - } - - case "VmStkCell": - // go further - - default: - return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", desc.StackType, v.SumType) - } - - tgcBoc, err := v.VmStkCell.Value.ToBocCustom(false, false, false, 0) - if err != nil { - return nil, errors.Wrap(err, "convert stack cell to boc") - } - c, err := cell.FromBOC(tgcBoc) - if err != nil { - return nil, errors.Wrap(err, "convert boc to cell") - } - - if desc.Format == "" && len(desc.Fields) > 0 { - desc.Format = TLBStructCell - } - +func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { switch desc.Format { - case "", TLBCell: + case TLBCell: return c, nil + + case TLBSlice: + return c.BeginParse(), nil + case TLBString: s, err := c.BeginParse().LoadStringSnake() if err != nil { return nil, errors.Wrap(err, "load string snake") } return s, nil + case TLBAddr: a, err := c.BeginParse().LoadAddr() if err != nil { return nil, errors.Wrap(err, "load address") } return a, nil + case TLBContentCell: content, err := nft.ContentFromCell(c) if err != nil { return nil, errors.Wrap(err, "load content from cell") } return content, nil + case 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) } return parsed, nil + default: d, ok := registeredDefinitions[desc.Format] if !ok { @@ -351,6 +326,45 @@ func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { } } +func vmParseValueCell(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { + switch v.SumType { + case "VmStkNull": + switch desc.Format { + case "", TLBCell, TLBStructCell: + return (*cell.Cell)(nil), nil + case TLBString: + return "", nil + case TLBContentCell: + return nft.ContentAny(nil), nil + default: + return nil, fmt.Errorf("unsupported '%s' format for '%s' type", desc.Format, desc.StackType) + } + + case "VmStkCell": + // go further + + default: + return nil, fmt.Errorf("wrong descriptor '%s' type as method returned '%s'", desc.StackType, v.SumType) + } + + tgcBoc, err := v.VmStkCell.Value.ToBocCustom(false, false, false, 0) + if err != nil { + return nil, errors.Wrap(err, "convert stack cell to boc") + } + c, err := cell.FromBOC(tgcBoc) + if err != nil { + return nil, errors.Wrap(err, "convert boc to cell") + } + + if desc.Format == "" && len(desc.Fields) > 0 { + desc.Format = TLBStructCell + } else if desc.Format == "" { + desc.Format = TLBCell + } + + return vmParseCell(c, desc) +} + func vmParseValueSlice(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { switch v.SumType { case "VmStkNull": @@ -383,36 +397,11 @@ func vmParseValueSlice(v *tlb.VmStackValue, desc *VmValueDesc) (any, error) { if desc.Format == "" && len(desc.Fields) > 0 { desc.Format = TLBStructCell + } else if desc.Format == "" { + desc.Format = TLBSlice } - switch desc.Format { - case "": - return c.BeginParse(), nil - case TLBString: - return c.BeginParse().LoadStringSnake() - case TLBAddr: - a, err := c.BeginParse().LoadAddr() - if err != nil { - return nil, errors.Wrap(err, "load address") - } - return a, nil - case TLBStructCell: - parsed, err := desc.Fields.FromCell(c) - if err != nil { - return nil, errors.Wrapf(err, "load struct from slice on %s value description schema", desc.Name) - } - return parsed, nil - default: - d, ok := registeredDefinitions[desc.Format] - if !ok { - return nil, fmt.Errorf("cannot find definition for '%s' format", desc.Format) - } - parsed, err := d.FromCell(c) - if err != nil { - return nil, errors.Wrapf(err, "'%s' definition from cell", desc.Format) - } - return parsed, nil - } + return vmParseCell(c, desc) } func vmParseValue(v *tlb.VmStackValue, d *VmValueDesc) (any, error) { diff --git a/abi/tlb_types.go b/abi/tlb_types.go index 810aee9c..472c1e17 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -20,6 +20,7 @@ const ( TLBString TLBType = "string" TLBBytes TLBType = "bytes" TLBCell TLBType = "cell" + TLBSlice TLBType = "slice" TLBContentCell TLBType = "content" TLBStructCell TLBType = "struct" TLBTag TLBType = "tag" From 879caf0c5f6edd173fea60f53906deb18d796f13 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Sat, 14 Oct 2023 15:52:52 +0530 Subject: [PATCH 026/186] [api] return definitions --- api/http/docs.go | 94 +++++++++++++++++++++++++++------ api/http/swagger.json | 94 +++++++++++++++++++++++++++------ api/http/swagger.yaml | 71 ++++++++++++++++++++----- internal/api/http/controller.go | 24 +++++++++ internal/api/http/server.go | 2 + internal/app/query.go | 2 + internal/app/query/query.go | 5 ++ 7 files changed, 246 insertions(+), 46 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index c62bd5e6..5f924967 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -305,6 +305,29 @@ const docTemplate = `{ } } }, + "/contracts/definitions": { + "get": { + "description": "Returns definitions used in messages and get-methods parsing", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contract" + ], + "summary": "struct definitions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.GetDefinitionsRes" + } + } + } + } + }, "/contracts/interfaces": { "get": { "description": "Returns known contract interfaces", @@ -984,31 +1007,19 @@ const docTemplate = `{ "enum": [ "int", "cell", - "slice", - "addr", - "bool", - "bigInt", - "string", - "bytes", - "content" + "slice" ], "x-enum-varnames": [ "VmInt", "VmCell", - "VmSlice", - "VmAddr", - "VmBool", - "VmBigInt", - "VmString", - "VmBytes", - "VmContentCell" + "VmSlice" ] }, "abi.TLBFieldDesc": { "type": "object", "properties": { "format": { - "type": "string" + "$ref": "#/definitions/abi.TLBType" }, "name": { "type": "string" @@ -1035,17 +1046,51 @@ const docTemplate = `{ "$ref": "#/definitions/abi.TLBFieldDesc" } }, + "abi.TLBType": { + "type": "string", + "enum": [ + "addr", + "bool", + "bigInt", + "string", + "bytes", + "cell", + "slice", + "content", + "struct", + "tag" + ], + "x-enum-varnames": [ + "TLBAddr", + "TLBBool", + "TLBBigInt", + "TLBString", + "TLBBytes", + "TLBCell", + "TLBSlice", + "TLBContentCell", + "TLBStructCell", + "TLBTag" + ] + }, "abi.VmValueDesc": { "type": "object", "properties": { "format": { - "$ref": "#/definitions/abi.StackType" + "$ref": "#/definitions/abi.TLBType" }, "name": { "type": "string" }, "stack_type": { "$ref": "#/definitions/abi.StackType" + }, + "struct_fields": { + "description": "Format = \"struct\"", + "type": "array", + "items": { + "$ref": "#/definitions/abi.TLBFieldDesc" + } } } }, @@ -1943,6 +1988,23 @@ const docTemplate = `{ } } }, + "http.GetDefinitionsRes": { + "type": "object", + "properties": { + "results": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/abi.TLBFieldDesc" + } + } + }, + "total": { + "type": "integer" + } + } + }, "http.GetInterfacesRes": { "type": "object", "properties": { diff --git a/api/http/swagger.json b/api/http/swagger.json index 08c21506..2b55b1d0 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -302,6 +302,29 @@ } } }, + "/contracts/definitions": { + "get": { + "description": "Returns definitions used in messages and get-methods parsing", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contract" + ], + "summary": "struct definitions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.GetDefinitionsRes" + } + } + } + } + }, "/contracts/interfaces": { "get": { "description": "Returns known contract interfaces", @@ -981,31 +1004,19 @@ "enum": [ "int", "cell", - "slice", - "addr", - "bool", - "bigInt", - "string", - "bytes", - "content" + "slice" ], "x-enum-varnames": [ "VmInt", "VmCell", - "VmSlice", - "VmAddr", - "VmBool", - "VmBigInt", - "VmString", - "VmBytes", - "VmContentCell" + "VmSlice" ] }, "abi.TLBFieldDesc": { "type": "object", "properties": { "format": { - "type": "string" + "$ref": "#/definitions/abi.TLBType" }, "name": { "type": "string" @@ -1032,17 +1043,51 @@ "$ref": "#/definitions/abi.TLBFieldDesc" } }, + "abi.TLBType": { + "type": "string", + "enum": [ + "addr", + "bool", + "bigInt", + "string", + "bytes", + "cell", + "slice", + "content", + "struct", + "tag" + ], + "x-enum-varnames": [ + "TLBAddr", + "TLBBool", + "TLBBigInt", + "TLBString", + "TLBBytes", + "TLBCell", + "TLBSlice", + "TLBContentCell", + "TLBStructCell", + "TLBTag" + ] + }, "abi.VmValueDesc": { "type": "object", "properties": { "format": { - "$ref": "#/definitions/abi.StackType" + "$ref": "#/definitions/abi.TLBType" }, "name": { "type": "string" }, "stack_type": { "$ref": "#/definitions/abi.StackType" + }, + "struct_fields": { + "description": "Format = \"struct\"", + "type": "array", + "items": { + "$ref": "#/definitions/abi.TLBFieldDesc" + } } } }, @@ -1940,6 +1985,23 @@ } } }, + "http.GetDefinitionsRes": { + "type": "object", + "properties": { + "results": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/abi.TLBFieldDesc" + } + } + }, + "total": { + "type": "integer" + } + } + }, "http.GetInterfacesRes": { "type": "object", "properties": { diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index a89e4087..3c294b0f 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -52,27 +52,15 @@ definitions: - int - cell - slice - - addr - - bool - - bigInt - - string - - bytes - - content type: string x-enum-varnames: - VmInt - VmCell - VmSlice - - VmAddr - - VmBool - - VmBigInt - - VmString - - VmBytes - - VmContentCell abi.TLBFieldDesc: properties: format: - type: string + $ref: '#/definitions/abi.TLBType' name: type: string optional: @@ -88,14 +76,43 @@ definitions: items: $ref: '#/definitions/abi.TLBFieldDesc' type: array + abi.TLBType: + enum: + - addr + - bool + - bigInt + - string + - bytes + - cell + - slice + - content + - struct + - tag + type: string + x-enum-varnames: + - TLBAddr + - TLBBool + - TLBBigInt + - TLBString + - TLBBytes + - TLBCell + - TLBSlice + - TLBContentCell + - TLBStructCell + - TLBTag abi.VmValueDesc: properties: format: - $ref: '#/definitions/abi.StackType' + $ref: '#/definitions/abi.TLBType' name: type: string stack_type: $ref: '#/definitions/abi.StackType' + struct_fields: + description: Format = "struct" + items: + $ref: '#/definitions/abi.TLBFieldDesc' + type: array type: object aggregate.AccountsRes: properties: @@ -686,6 +703,17 @@ definitions: type: object type: array type: object + http.GetDefinitionsRes: + properties: + results: + additionalProperties: + items: + $ref: '#/definitions/abi.TLBFieldDesc' + type: array + type: object + total: + type: integer + type: object http.GetInterfacesRes: properties: results: @@ -917,6 +945,21 @@ paths: summary: block info tags: - block + /contracts/definitions: + get: + consumes: + - application/json + description: Returns definitions used in messages and get-methods parsing + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.GetDefinitionsRes' + summary: struct definitions + tags: + - contract /contracts/interfaces: get: consumes: diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index 46cc084e..a58f289c 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" @@ -191,6 +192,29 @@ func (c *Controller) GetOperations(ctx *gin.Context) { ctx.IndentedJSON(http.StatusOK, GetOperationsRes{Total: len(ret), Results: ret}) } +type GetDefinitionsRes struct { + Total int `json:"total"` + Results map[abi.TLBType]abi.TLBFieldsDesc `json:"results"` +} + +// GetDefinitions godoc +// +// @Summary struct definitions +// @Description Returns definitions used in messages and get-methods parsing +// @Tags contract +// @Accept json +// @Produce json +// @Success 200 {object} GetDefinitionsRes +// @Router /contracts/definitions [get] +func (c *Controller) GetDefinitions(ctx *gin.Context) { + ret, err := c.svc.GetDefinitions(ctx) + if err != nil { + internalErr(ctx, err) + return + } + ctx.IndentedJSON(http.StatusOK, GetDefinitionsRes{Total: len(ret), Results: ret}) +} + // GetBlocks godoc // // @Summary block info diff --git a/internal/api/http/server.go b/internal/api/http/server.go index b9f9791e..10038e79 100644 --- a/internal/api/http/server.go +++ b/internal/api/http/server.go @@ -34,6 +34,7 @@ type QueryController interface { GetInterfaces(*gin.Context) GetOperations(*gin.Context) + GetDefinitions(*gin.Context) } type Server struct { @@ -75,6 +76,7 @@ func (s *Server) RegisterRoutes(t QueryController) { base.GET("/contracts/interfaces", t.GetInterfaces) base.GET("/contracts/operations", t.GetOperations) + base.GET("/contracts/definitions", t.GetDefinitions) base.GET("/swagger/*any", ginSwagger.WrapHandler( swaggerFiles.Handler, diff --git a/internal/app/query.go b/internal/app/query.go index b449bb02..77ea09ae 100644 --- a/internal/app/query.go +++ b/internal/app/query.go @@ -5,6 +5,7 @@ import ( "github.com/xssnick/tonutils-go/ton" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/aggregate" "github.com/tonindexer/anton/internal/core/aggregate/history" @@ -21,6 +22,7 @@ type QueryConfig struct { type QueryService interface { GetStatistics(ctx context.Context) (*aggregate.Statistics, error) + GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) GetOperations(ctx context.Context) ([]*core.ContractOperation, error) diff --git a/internal/app/query/query.go b/internal/app/query/query.go index 1b868e5b..20637016 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/xssnick/tonutils-go/ton" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/app/fetcher" @@ -47,6 +48,10 @@ func NewService(_ context.Context, cfg *app.QueryConfig) (*Service, error) { return s, nil } +func (s *Service) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { + return s.contractRepo.GetDefinitions(ctx) +} + func (s *Service) GetStatistics(ctx context.Context) (*aggregate.Statistics, error) { return aggregate.GetStatistics(ctx, s.DB.CH, s.DB.PG) } From c94ca41b810c66a7b36e339ebd1698b398c53ab0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 16 Oct 2023 12:19:59 +0530 Subject: [PATCH 027/186] [abi] parse dictionaries into map --- abi/tlb.go | 35 ++++++++++++++------ abi/tlb_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 6 ++-- 4 files changed, 118 insertions(+), 13 deletions(-) diff --git a/abi/tlb.go b/abi/tlb.go index 31ae1e7b..dea4c315 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -108,16 +108,25 @@ func tlbParseSettingsDict(settings []string) (reflect.Type, error) { return nil, fmt.Errorf("wrong dict settings: %v", settings) } + if settings[1] == "inline" { + settings = settings[1:] + } + _, err := strconv.ParseUint(settings[1], 10, 64) if err != nil { return nil, fmt.Errorf("cannot deserialize field as dict, bad size '%s'", settings[1]) } - if len(settings) >= 4 { - // transformation - return nil, errors.New("dict transformation is not supported") + if len(settings) < 4 { + return reflect.TypeOf((*cell.Dictionary)(nil)), nil + } + + mapVT, err := tlbParseSettings(strings.Join(settings[3:], " ")) + if err != nil { + return nil, err } - return reflect.TypeOf((*cell.Dictionary)(nil)), nil + + return reflect.MapOf(reflect.TypeOf(""), mapVT), nil } // tlbParseSettings automatically determines go type to map field into (refactor of tlb.LoadFromCell) @@ -188,7 +197,7 @@ func tlbParseSettings(tag string) (reflect.Type, error) { } } -func tlbMapFormat(format TLBType, tag reflect.StructTag) (reflect.Type, error) { +func tlbMapFormat(format TLBType, tag string) (reflect.Type, error) { t, ok := typeNameMap[format] if ok { return t, nil @@ -197,9 +206,9 @@ func tlbMapFormat(format TLBType, tag reflect.StructTag) (reflect.Type, error) { switch format { case "": // parse tlb tag and get default type - t, err := tlbParseSettings(tag.Get("tlb")) + t, err := tlbParseSettings(tag) if t == nil || err != nil { - return nil, fmt.Errorf("parse tlb settings with tag %s: %w", tag.Get("tlb"), err) + return nil, fmt.Errorf("parse tlb settings with tag '%s': %w", tag, err) } return t, nil @@ -208,11 +217,17 @@ func tlbMapFormat(format TLBType, tag reflect.StructTag) (reflect.Type, error) { if !ok { return nil, fmt.Errorf("cannot find definition for '%s' format", format) } + t, err := tlbParseDesc(nil, d) if err != nil { return nil, errors.Wrap(err, "creating new type from definition") } - return reflect.PointerTo(t), nil + vt := reflect.PointerTo(t) + + if !strings.HasPrefix(tag, "dict") { + return vt, nil + } + return reflect.MapOf(reflect.TypeOf(""), vt), nil } } @@ -241,7 +256,7 @@ func tlbParseDesc(fields []reflect.StructField, schema TLBFieldsDesc, skipOption } sf.Type = reflect.PointerTo(sf.Type) } else { - sf.Type, err = tlbMapFormat(f.Format, sf.Tag) + sf.Type, err = tlbMapFormat(f.Format, sf.Tag.Get("tlb")) if err != nil { return nil, errors.Wrapf(err, "%s field", f.Name) } @@ -274,7 +289,7 @@ func (desc TLBFieldsDesc) FromCell(c *cell.Cell) (any, error) { if err != nil { return nil, errors.Wrapf(err, "creating struct") } - if err := tlb.LoadFromCell(parsed, c.BeginParse()); err == nil { + if err = tlb.LoadFromCell(parsed, c.BeginParse()); err == nil { return parsed, nil } if !strings.Contains(err.Error(), "not enough data in reader") && !strings.Contains(err.Error(), "no more refs exists") { diff --git a/abi/tlb_test.go b/abi/tlb_test.go index f2b7ba62..25c682ba 100644 --- a/abi/tlb_test.go +++ b/abi/tlb_test.go @@ -112,6 +112,94 @@ func TestTLBFieldsDesc_LoadFromCell(t *testing.T) { require.Equal(t, exp, string(j)) } +func TestTLBFieldsDesc_LoadFromCell_DictToMap(t *testing.T) { + d := []byte(`[ + { + "name": "order_tag", + "tlb_type": "$0010", + "format": "tag" + }, + { + "name": "expiration", + "tlb_type": "## 32" + }, + { + "name": "direction", + "tlb_type": "## 1" + }, + { + "name": "amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "leverage", + "tlb_type": "## 64" + }, + { + "name": "limit_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "stop_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "stop_trigger_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "take_trigger_price", + "tlb_type": ".", + "format": "coins" + } +]`) + + var descD abi.TLBFieldsDesc + + err := json.Unmarshal(d, &descD) + require.Nil(t, err) + + err = abi.RegisterDefinitions(map[abi.TLBType]abi.TLBFieldsDesc{ + "take_order": descD, + }) + if err != nil { + require.Nil(t, err) + } + + j := []byte(`[ + { + "name": "dict_uint_3", + "tlb_type": "dict inline 3 -> ^", + "format": "take_order" + } +]`) + + var desc abi.TLBFieldsDesc + + err = json.Unmarshal(j, &desc) + require.Nil(t, err) + + body, err := base64.StdEncoding.DecodeString(`te6cckEBBQEAUwACAdQDAQEBIAIAQSZS6uXai6Q7dAAAAAAAWWgvACEeGjAAIU3JOAIO5rKAQAEBIAQAQSZS5ufKi6Q7dAAAAAAAWWgvACEeGjAAIU3JOAIO5rKAQPxznzQ=`) + require.Nil(t, err) + + c, err := cell.FromBOC(body) + require.Nil(t, err) + + got, err := desc.FromCell(c) + require.Nil(t, err) + + j, err = json.Marshal(got) + require.Nil(t, err) + + require.Equal(t, + `{"dict_uint_3":{"0":{"order_tag":{},"expiration":1697541756,"direction":1,"amount":"100000000000","leverage":3000000000,"limit_price":"600000000","stop_price":"0","stop_trigger_price":"700000000","take_trigger_price":"500000000"},"1":{"order_tag":{},"expiration":1697558109,"direction":1,"amount":"100000000000","leverage":3000000000,"limit_price":"600000000","stop_price":"0","stop_trigger_price":"700000000","take_trigger_price":"500000000"}}}`, + string(j)) +} + func TestTLBFieldsDesc_LoadFromCell_DefinitionsUnion(t *testing.T) { j := []byte(`{ "interface": "jetton_vault", diff --git a/go.mod b/go.mod index ed9c9121..eccb2d07 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.8.5-0.20231013083545-4f085f4fc80f + github.com/xssnick/tonutils-go v1.8.5-0.20231016063454-6d3e0636946d ) require github.com/gin-contrib/cors v1.4.0 diff --git a/go.sum b/go.sum index 0d526537..5f1de5de 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0 h1:0/RkLzp6whu/zFCBlEgQyAkbj4QbOlObn8jYQuhm4vg= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0/go.mod h1:ZkFYp+b3tn7YiHR6yMnHqGetPfFZhbVYVTsTGBIbdCY= +github.com/iam047801/tonutils-go v0.0.0-20231015152158-8d07daf52f7a h1:sMViB3ah4Qx0d/13rQIeORK0c4AlR3YKzt2Eukvltbc= +github.com/iam047801/tonutils-go v0.0.0-20231015152158-8d07daf52f7a/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -184,8 +186,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.8.5-0.20231013083545-4f085f4fc80f h1:0wxnLLvfDe0soBkITECdT9D2DmcBMIe9Xzv3+gi0W6o= -github.com/xssnick/tonutils-go v1.8.5-0.20231013083545-4f085f4fc80f/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= +github.com/xssnick/tonutils-go v1.8.5-0.20231016063454-6d3e0636946d h1:M9SaxaPlxCeGEAsdu6yyypjB208e2g7mPymts+0JZC4= +github.com/xssnick/tonutils-go v1.8.5-0.20231016063454-6d3e0636946d/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= From a93c77283ba1423d823d9c63bc17e6c9141b2de9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 16 Oct 2023 12:44:10 +0530 Subject: [PATCH 028/186] [abi] add test for definitions parsing in get-method return values --- abi/get_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/abi/get_test.go b/abi/get_test.go index ad483048..6c3f8157 100644 --- a/abi/get_test.go +++ b/abi/get_test.go @@ -3,6 +3,7 @@ package abi_test import ( "context" "encoding/base64" + "encoding/json" "math/big" "testing" @@ -15,6 +16,20 @@ import ( "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") @@ -39,13 +54,7 @@ func TestGetMethodHashes(t *testing.T) { require.Equal(t, []int32{0x18fcf}, hashes) } -func TestNewEmulator_RunGetMethod(t *testing.T) { - // mainnet blockchain config - config, err := base64.StdEncoding.DecodeString("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") - require.Nil(t, err) - configCell, err := cell.FromBOCMultiRoot(config) - require.Nil(t, err) - +func TestEmulator_RunGetMethod(t *testing.T) { // query nft collection get_nft_address_by_index collection := address.MustParseAddr("EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg") @@ -59,7 +68,7 @@ func TestNewEmulator_RunGetMethod(t *testing.T) { collectionDataCell, err := cell.FromBOCMultiRoot(collectionData) require.Nil(t, err) - eCollection, err := abi.NewEmulator(collection, collectionCodeCell[0], collectionDataCell[0], configCell[0]) + 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", @@ -97,7 +106,7 @@ func TestNewEmulator_RunGetMethod(t *testing.T) { itemDataCell, err := cell.FromBOCMultiRoot(itemData) require.Nil(t, err) - eItem, err := abi.NewEmulator(item, itemCodeCell[0], itemDataCell[0], configCell[0]) + eItem, err := abi.NewEmulator(item, itemCodeCell[0], itemDataCell[0], configCell) require.Nil(t, err) ret, err = eItem.RunGetMethod(context.Background(), "get_nft_data", nil, @@ -164,3 +173,79 @@ func TestNewEmulator_RunGetMethod(t *testing.T) { 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)) +} From 4981e478db2f02d99aab8c0bc9fb25e3121d44b9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 16 Oct 2023 13:19:49 +0530 Subject: [PATCH 029/186] [abi] update readme with dictionary mappings --- abi/README.md | 87 +++++++++++++++++++++++++++++-- abi/tlb.go | 5 +- abi/tlb_test.go | 132 +++++++++++++++++++++++++++++------------------- 3 files changed, 167 insertions(+), 57 deletions(-) diff --git a/abi/README.md b/abi/README.md index 129d39d6..08727c24 100644 --- a/abi/README.md +++ b/abi/README.md @@ -55,8 +55,6 @@ Also, it is possible to define similarly described embedded structures in each f } ``` -### Types mapping - While parsing TL-B cells by fields description, we are trying to parse data according to TL-B type and map it into some Golang type or structure. Each TL-B type used in schemas has value equal to the structure tags in [tonutils-go](https://github.com/xssnick/tonutils-go/blob/4d0157009913e35d450c36e28018cd0686502439/tlb/loader.go#L24). If it is not possible to parse the field using `tlb.LoadFromCell`, @@ -66,7 +64,7 @@ Accepted TL-B types in `tlb_type`: 1. `## N` - integer with N bits; by default maps to `uintX` or `big.Int` 2. `^` - data is stored in the referenced cell; by default maps to `cell.Cell` or to custom struct, if `struct_fields` is defined 3. `.` - inner struct; by default maps to `cell.Cell` or to custom struct, if `struct_fields` is defined -4. [TODO] `[^]dict N [-> array [^]]` - dictionary with key size `N`, transformation is not supported yet +4. `[^]dict [inline] N [-> [^]]` - dictionary with key size `N`, transformation to `map` is done through `->` 5. `bits N` - bit slice N len; by default maps to `[]byte` 6. `bool` - 1 bit boolean; by default maps to `bool` 7. `addr` - ton address; by default maps to `addr.Address` @@ -151,7 +149,7 @@ Accepted return values stack types: 3. `slice` - load slice 4. [TODO] `tuple` -Accepted types to map from or into in `format` field: +Accepted types to map from or parse into in `format` field: 1. `addr` - MsgAddress slice type 2. `bool` - map int to boolean @@ -204,7 +202,7 @@ You can use those definitions in message schemas: } ``` -Or get-method stack values schemas: +Or use them in get-method return values' schema: ```json5 { @@ -363,6 +361,85 @@ After parsing `deposit_liquidity` transfer notification message body will look l } ``` +### Dictionary transformation + +You can define the format of the dictionary values, so Anton will be able to parse it into the golang `map`. + +In the following example, we use defined `limit_order` as a dictionary value: +```json5 +{ + // ... + "definitions": { + "limit_order": [ + { + "name": "order_tag", + "tlb_type": "$0010", + "format": "tag" + }, + { + "name": "expiration", + "tlb_type": "## 32" + }, + // ... + ] + }, + // ... + "in_message": { + // ... + "body": [ + { + "name": "dict_3_bit_key", + "tlb_type": "dict inline 3 -> ^", + "format": "limit_order" + } + ] + } +} +``` + +Or we can use defined `orders` union as a dictionary value, but for the union we're setting `tlb_type` field instead of `format`. +```json5 +{ + // ... + "definitions": { + "take_order": [ + { + "name": "take_order_tag", + "tlb_type": "$0001", + "format": "tag" + }, + { + "name": "expiration", + "tlb_type": "## 32" + }, + // ... + ], + "limit_order": [ + { + "name": "order_tag", + "tlb_type": "$0010", + "format": "tag" + }, + { + "name": "expiration", + "tlb_type": "## 32" + }, + // ... + ] + }, + // ... + "in_message": { + // ... + "body": [ + { + "name": "dict_3_bit_key", + "tlb_type": "dict inline 3 -> ^ [take_order,limit_order]" + } + ] + } +} +``` + ## Known contracts 1. TEP-62 NFT Standard: [interfaces](/abi/known/tep62_nft.json), [description](https://github.com/ton-blockchain/TEPs/blob/master/text/0062-nft-standard.md), [contract code](https://github.com/ton-blockchain/token-contract/tree/main/nft) diff --git a/abi/tlb.go b/abi/tlb.go index dea4c315..ecd9300b 100644 --- a/abi/tlb.go +++ b/abi/tlb.go @@ -187,7 +187,10 @@ func tlbParseSettings(tag string) (reflect.Type, error) { return reflect.TypeOf([]byte(nil)), nil case "^", ".": - return reflect.TypeOf((*cell.Cell)(nil)), nil + if len(settings) == 1 { + return reflect.TypeOf((*cell.Cell)(nil)), nil + } + return tlbParseSettings(strings.Join(settings[1:], " ")) case "dict": return tlbParseSettingsDict(settings) diff --git a/abi/tlb_test.go b/abi/tlb_test.go index 25c682ba..a34852f4 100644 --- a/abi/tlb_test.go +++ b/abi/tlb_test.go @@ -113,59 +113,67 @@ func TestTLBFieldsDesc_LoadFromCell(t *testing.T) { } func TestTLBFieldsDesc_LoadFromCell_DictToMap(t *testing.T) { - d := []byte(`[ - { - "name": "order_tag", - "tlb_type": "$0010", - "format": "tag" - }, - { - "name": "expiration", - "tlb_type": "## 32" - }, - { - "name": "direction", - "tlb_type": "## 1" - }, - { - "name": "amount", - "tlb_type": ".", - "format": "coins" - }, - { - "name": "leverage", - "tlb_type": "## 64" - }, - { - "name": "limit_price", - "tlb_type": ".", - "format": "coins" - }, - { - "name": "stop_price", - "tlb_type": ".", - "format": "coins" - }, - { - "name": "stop_trigger_price", - "tlb_type": ".", - "format": "coins" - }, - { - "name": "take_trigger_price", - "tlb_type": ".", - "format": "coins" - } -]`) + d := []byte(` +{ + "take_order": [ + { + "name": "order_tag", + "tlb_type": "$0010", + "format": "tag" + }, + { + "name": "expiration", + "tlb_type": "## 32" + }, + { + "name": "direction", + "tlb_type": "## 1" + }, + { + "name": "amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "leverage", + "tlb_type": "## 64" + }, + { + "name": "limit_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "stop_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "stop_trigger_price", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "take_trigger_price", + "tlb_type": ".", + "format": "coins" + } + ], + "limit_order": [ + { + "name": "order_tag", + "tlb_type": "$0001", + "format": "tag" + } + ] +}`) - var descD abi.TLBFieldsDesc + var descD map[abi.TLBType]abi.TLBFieldsDesc err := json.Unmarshal(d, &descD) require.Nil(t, err) - err = abi.RegisterDefinitions(map[abi.TLBType]abi.TLBFieldsDesc{ - "take_order": descD, - }) + err = abi.RegisterDefinitions(descD) if err != nil { require.Nil(t, err) } @@ -178,9 +186,9 @@ func TestTLBFieldsDesc_LoadFromCell_DictToMap(t *testing.T) { } ]`) - var desc abi.TLBFieldsDesc + var descF abi.TLBFieldsDesc - err = json.Unmarshal(j, &desc) + err = json.Unmarshal(j, &descF) require.Nil(t, err) body, err := base64.StdEncoding.DecodeString(`te6cckEBBQEAUwACAdQDAQEBIAIAQSZS6uXai6Q7dAAAAAAAWWgvACEeGjAAIU3JOAIO5rKAQAEBIAQAQSZS5ufKi6Q7dAAAAAAAWWgvACEeGjAAIU3JOAIO5rKAQPxznzQ=`) @@ -189,7 +197,29 @@ func TestTLBFieldsDesc_LoadFromCell_DictToMap(t *testing.T) { c, err := cell.FromBOC(body) require.Nil(t, err) - got, err := desc.FromCell(c) + got, err := descF.FromCell(c) + require.Nil(t, err) + + j, err = json.Marshal(got) + require.Nil(t, err) + + require.Equal(t, + `{"dict_uint_3":{"0":{"order_tag":{},"expiration":1697541756,"direction":1,"amount":"100000000000","leverage":3000000000,"limit_price":"600000000","stop_price":"0","stop_trigger_price":"700000000","take_trigger_price":"500000000"},"1":{"order_tag":{},"expiration":1697558109,"direction":1,"amount":"100000000000","leverage":3000000000,"limit_price":"600000000","stop_price":"0","stop_trigger_price":"700000000","take_trigger_price":"500000000"}}}`, + string(j)) + + j = []byte(`[ + { + "name": "dict_uint_3", + "tlb_type": "dict inline 3 -> ^ [take_order,limit_order]" + } +]`) + + var desc abi.TLBFieldsDesc + + err = json.Unmarshal(j, &desc) + require.Nil(t, err) + + got, err = desc.FromCell(c) require.Nil(t, err) j, err = json.Marshal(got) From 9d54dc37a5d11c3abdc1a1b5bec578ce7f8b3c30 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 30 Oct 2023 22:11:09 +0530 Subject: [PATCH 030/186] [migrations] clear contract definitions ch table --- .../20231014090410_contract_definitions_table.down.sql | 5 ----- .../20231014090410_contract_definitions_table.up.sql | 5 ----- 2 files changed, 10 deletions(-) diff --git a/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql b/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql index d4906491..e69de29b 100644 --- a/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql +++ b/migrations/chmigrations/20231014090410_contract_definitions_table.down.sql @@ -1,5 +0,0 @@ -SELECT 1 - ---migration:split - -SELECT 2 diff --git a/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql b/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql index d4906491..e69de29b 100644 --- a/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql +++ b/migrations/chmigrations/20231014090410_contract_definitions_table.up.sql @@ -1,5 +0,0 @@ -SELECT 1 - ---migration:split - -SELECT 2 From 6cc9d3583090d7f3d5c7fbfc410a2e4c68729936 Mon Sep 17 00:00:00 2001 From: stfy Date: Mon, 4 Dec 2023 16:21:14 +0300 Subject: [PATCH 031/186] [app] indexer: chore fixes, recursive peek libraries refs --- internal/app/fetcher/libraries.go | 23 ++++++++++++++++++++--- internal/core/tx.go | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index a01c9fe5..45af3de8 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -15,13 +15,11 @@ type LibDescription struct { func (s *Service) GetAccountLibraries(ctx context.Context, raw *tlb.Account) (*cell.Cell, error) { hashes, err := findLibraries(raw.Code) - if err != nil { return nil, errors.Wrapf(err, "find libraries") } libs, err := s.API.GetLibraries(ctx, hashes...) - if err != nil { return nil, errors.Wrapf(err, "get libraries") } @@ -59,7 +57,6 @@ func getLibraryHash(code *cell.Cell) ([]byte, error) { return hash[1:], nil } -// TODO recursive Refs func findLibraries(code *cell.Cell) ([][]byte, error) { hashes := make([][]byte, 0) @@ -71,6 +68,26 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { } hashes = append(hashes, hash) + + return hashes, err + } + + if code.RefsNum() == 0 { + return hashes, nil + } + + for i := code.RefsNum(); i < 0; i-- { + ref, err := code.PeekRef(int(i - 1)) + if err != nil { + return nil, err + } + + hash, err := findLibraries(ref) + if err != nil { + return nil, err + } + + hashes = append(hashes, hash...) } return hashes, nil diff --git a/internal/core/tx.go b/internal/core/tx.go index bd1f1562..a4175516 100644 --- a/internal/core/tx.go +++ b/internal/core/tx.go @@ -59,7 +59,7 @@ func (tx *Transaction) LoadDescription() error { // TODO: optionally load descri return errors.Wrap(err, "load description boc") } - if err := tlb.LoadFromCell(d, c.BeginParse()); err != nil { + if err := tlb.LoadFromCell(&d, c.BeginParse()); err != nil { return errors.Wrap(err, "load description from cell") } From 18632661f158a41b1130579a7661052d75bcd537 Mon Sep 17 00:00:00 2001 From: stfy Date: Mon, 4 Dec 2023 16:34:19 +0300 Subject: [PATCH 032/186] [app] indexer: chore fixes --- abi/get_emulator.go | 1 - internal/app/fetcher/account.go | 3 --- internal/app/fetcher/cache.go | 8 ++------ internal/app/fetcher/libraries.go | 8 +++----- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 319062f4..06cbd90d 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -49,7 +49,6 @@ type Emulator struct { func newEmulator(addr *address.Address, e *tvm.Emulator) (*Emulator, error) { accId, err := ton.AccountIDFromBase64Url(addr.String()) - if err != nil { return nil, errors.Wrap(err, "parse address") } diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 54e138d4..f833e05d 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -35,7 +35,6 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr if raw.Code != nil { libs, err := s.GetAccountLibraries(ctx, raw) - if err != nil { return nil, errors.Wrapf(err, "get account libraries") } @@ -46,13 +45,11 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr if raw.Code.GetType() == cell.LibraryCellType { hash, err := getLibraryHash(raw.Code) - if err != nil { return nil, errors.Wrap(err, "get library hash") } lib := s.libraries.get(hash) - if lib != nil { acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) } diff --git a/internal/app/fetcher/cache.go b/internal/app/fetcher/cache.go index 620bf47e..2be166cb 100644 --- a/internal/app/fetcher/cache.go +++ b/internal/app/fetcher/cache.go @@ -124,15 +124,13 @@ func (c *accountCache) set(bExt *ton.BlockIDExt, acc *core.AccountState) { } type librariesCache struct { - libs map[string]*LibDescription - lastCleared time.Time + libs map[string]*LibDescription sync.Mutex } func newLibrariesCache() *librariesCache { return &librariesCache{ - libs: map[string]*LibDescription{}, - lastCleared: time.Time{}, + libs: map[string]*LibDescription{}, } } @@ -141,7 +139,6 @@ func (c *librariesCache) get(hash []byte) *LibDescription { defer c.Unlock() l, ok := c.libs[hex.EncodeToString(hash)] - if ok { return l } @@ -156,7 +153,6 @@ func (c *librariesCache) set(hash []byte, desc *LibDescription) { h := hex.EncodeToString(hash) _, ok := c.libs[h] - if ok { return } diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index 45af3de8..edd7aed3 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -31,17 +31,15 @@ func (s *Service) GetAccountLibraries(ctx context.Context, raw *tlb.Account) (*c h := cell.BeginCell().MustStoreSlice(hash, 256).EndCell() t, err := tlb.ToCell(desc) - if err != nil { return nil, err } - err = libsMap.Set(h, t) - s.libraries.set(hash, &desc) - - if err != nil { + if err = libsMap.Set(h, t); err != nil { return nil, err } + + s.libraries.set(hash, &desc) } return libsMap.ToCell() From d730a8868f90dd76781b698c4e37dc445abf02f1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 20:32:40 +0530 Subject: [PATCH 033/186] Dockerfile: add readline, secp256k1 and sodium libraries for emulator build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4a1ecb82..c1fed348 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN apk add --no-cache make cmake gcc g++ musl-dev zlib-dev openssl-dev linux-he ADD --keep-git-dir=true https://github.com/ton-blockchain/ton.git /ton RUN cd /ton && git submodule update --init --recursive -RUN apk add --no-cache openblas-dev libmicrohttpd-dev +RUN apk add --no-cache openblas-dev libmicrohttpd-dev readline-dev libsecp256k1-dev libsodium-dev RUN mkdir build && (cd build && cmake ../ton -DCMAKE_BUILD_TYPE=Release && cmake --build . --target emulator -- -j 8) RUN mkdir /output && cp build/emulator/libemulator.so /output From 5db009088d1d738e1cad4444a32a43a6d2348751 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 20:39:56 +0530 Subject: [PATCH 034/186] Dockerfile: add secp256k1 and sodium libraries for anton build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c1fed348..802939a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN mkdir /output && cp build/emulator/libemulator.so /output # build FROM golang:1.19-alpine AS builder -RUN apk add --no-cache build-base +RUN apk add --no-cache build-base libsecp256k1 libsodium #prepare env WORKDIR /go/src/github.com/tonindexer/anton From 315b7fc7a5299e6fde8af50f8fee2a86c2f8fe86 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 20:41:47 +0530 Subject: [PATCH 035/186] Dockerfile: add secp256k1 and sodium libraries for application --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 802939a1..2118c55a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ FROM alpine:3 ENV LISTEN=0.0.0.0:8080 -RUN apk add --no-cache libgcc libstdc++ +RUN apk add --no-cache libgcc libstdc++ libsecp256k1 libsodium RUN addgroup -S anton && adduser -S anton -G anton WORKDIR /app From 3f974db265c80672045ef65651f07ebf85ec8b26 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 21:54:08 +0530 Subject: [PATCH 036/186] update tonutils-go --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index eccb2d07..a4e1b10d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.8.5-0.20231016063454-6d3e0636946d + github.com/xssnick/tonutils-go v1.8.6-0.20231204114749-39872ea7b254 ) require github.com/gin-contrib/cors v1.4.0 diff --git a/go.sum b/go.sum index 5f1de5de..e9e28ffe 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0 h1:0/RkLzp6whu/zFCBlEgQyAkbj4QbOlObn8jYQuhm4vg= github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0/go.mod h1:ZkFYp+b3tn7YiHR6yMnHqGetPfFZhbVYVTsTGBIbdCY= -github.com/iam047801/tonutils-go v0.0.0-20231015152158-8d07daf52f7a h1:sMViB3ah4Qx0d/13rQIeORK0c4AlR3YKzt2Eukvltbc= -github.com/iam047801/tonutils-go v0.0.0-20231015152158-8d07daf52f7a/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -186,8 +184,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.8.5-0.20231016063454-6d3e0636946d h1:M9SaxaPlxCeGEAsdu6yyypjB208e2g7mPymts+0JZC4= -github.com/xssnick/tonutils-go v1.8.5-0.20231016063454-6d3e0636946d/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= +github.com/xssnick/tonutils-go v1.8.6-0.20231204114749-39872ea7b254 h1:59HrgAFn6woL1wa6lnwIMJXDHbFuksYnlsGRhCfMJBQ= +github.com/xssnick/tonutils-go v1.8.6-0.20231204114749-39872ea7b254/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= From 98000cd3c6bd24fcf3d2f445109ac716c9a6adbb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 22:10:15 +0530 Subject: [PATCH 037/186] Dockerfile: use debian --- Dockerfile | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2118c55a..fde6cfff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,30 @@ # syntax=docker/dockerfile:1.5-labs -FROM alpine:3 AS emulator-builder +FROM debian:12.2-slim AS emulator-builder + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Etc/UTC # build emulator libraries -RUN apk add --no-cache make cmake gcc g++ musl-dev zlib-dev openssl-dev linux-headers git +RUN apt-get update && \ + apt-get install -yqq \ + tzdata build-essential cmake clang openssl \ + libssl-dev zlib1g-dev gperf wget git curl \ + libreadline-dev ccache libmicrohttpd-dev ninja-build pkg-config \ + libsecp256k1-dev libsodium-dev ADD --keep-git-dir=true https://github.com/ton-blockchain/ton.git /ton RUN cd /ton && git submodule update --init --recursive -RUN apk add --no-cache openblas-dev libmicrohttpd-dev readline-dev libsecp256k1-dev libsodium-dev - RUN mkdir build && (cd build && cmake ../ton -DCMAKE_BUILD_TYPE=Release && cmake --build . --target emulator -- -j 8) RUN mkdir /output && cp build/emulator/libemulator.so /output # build -FROM golang:1.19-alpine AS builder +FROM golang:1.21.4-bookworm AS builder -RUN apk add --no-cache build-base libsecp256k1 libsodium +RUN apt-get update && \ + apt-get install -y libsecp256k1-1 libsodium23 #prepare env WORKDIR /go/src/github.com/tonindexer/anton @@ -47,18 +54,20 @@ RUN go build -o /anton /go/src/github.com/tonindexer/anton # application -FROM alpine:3 +FROM debian:12.2-slim ENV LISTEN=0.0.0.0:8080 -RUN apk add --no-cache libgcc libstdc++ libsecp256k1 libsodium +RUN apt-get update && \ + apt-get install -y libsecp256k1-1 libsodium23 libssl3 + +RUN groupadd anton && useradd -g anton anton -RUN addgroup -S anton && adduser -S anton -G anton WORKDIR /app COPY --from=builder /lib/libemulator.so /lib -COPY --from=builder /go/src/github.com/tonindexer/anton/abi/known /var/anton/known +COPY --from=buildher /go/src/github.com/tonindexer/anton/abi/known /var/anton/known COPY --from=builder /anton /usr/bin/anton USER anton:anton EXPOSE 8080 -ENTRYPOINT ["/usr/bin/anton"] +ENTRYPOINT ["/usr/bin/anton"] \ No newline at end of file From 637e751bd2d29825590bc459fbf92475c3a3e0b7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 22:28:56 +0530 Subject: [PATCH 038/186] Dockerfile: fix typo --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index da246ee7..889a42d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,7 +65,7 @@ RUN groupadd anton && useradd -g anton anton WORKDIR /app COPY --from=builder /lib/libemulator.so /lib -COPY --from=buildher /go/src/github.com/tonindexer/anton/abi/known /var/anton/known +COPY --from=builder /go/src/github.com/tonindexer/anton/abi/known /var/anton/known COPY --from=builder /anton /usr/bin/anton USER anton:anton From ce84b3988d313bbb4837997581f7e3444f9df85f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 4 Dec 2023 22:36:54 +0530 Subject: [PATCH 039/186] go mod tidy --- go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index e9e28ffe..1fd6ec78 100644 --- a/go.sum +++ b/go.sum @@ -158,8 +158,8 @@ github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo= github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/tonkeeper/tongo v1.1.2 h1:TJNkm9KmyhnNJbUKOwiekgzjILcKWY7AzRs/sTYZXRQ= -github.com/tonkeeper/tongo v1.1.2/go.mod h1:LdOBjpUz6vLp1EdX3E0XLNks9YI5XMSqaQahfOMrBEY= +github.com/tonkeeper/tongo v1.3.0 h1:Rz4Nq3nkliL8MMcWfCQ7iEpcIjynrj3jelmcLcA84do= +github.com/tonkeeper/tongo v1.3.0/go.mod h1:LdOBjpUz6vLp1EdX3E0XLNks9YI5XMSqaQahfOMrBEY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= From ee59ffac306a24bd4e13b2c7bf726503a965748c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 5 Dec 2023 13:50:04 +0530 Subject: [PATCH 040/186] update tonutils-go to v1.8.7 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1ee91f59..365a5ccf 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.8.6-0.20231204114749-39872ea7b254 + github.com/xssnick/tonutils-go v1.8.7 ) require github.com/gin-contrib/cors v1.4.0 diff --git a/go.sum b/go.sum index 1fd6ec78..9659b67a 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,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.8.6-0.20231204114749-39872ea7b254 h1:59HrgAFn6woL1wa6lnwIMJXDHbFuksYnlsGRhCfMJBQ= -github.com/xssnick/tonutils-go v1.8.6-0.20231204114749-39872ea7b254/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= +github.com/xssnick/tonutils-go v1.8.7 h1:z6NxKNqDVbhS3lyAq2g3XHZhW+/d/DQsnYMBiTN84H0= +github.com/xssnick/tonutils-go v1.8.7/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= From ab08885a846c59a34301a410cfd4f6876fc46de5 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 5 Dec 2023 14:18:06 +0530 Subject: [PATCH 041/186] update tonutils-go to v1.8.8-dev --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 365a5ccf..90f2bf6e 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 github.com/uptrace/go-clickhouse v0.3.0 github.com/urfave/cli/v2 v2.25.1 - github.com/xssnick/tonutils-go v1.8.7 + github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7 ) require github.com/gin-contrib/cors v1.4.0 diff --git a/go.sum b/go.sum index 9659b67a..8b625795 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xssnick/tonutils-go v1.8.7 h1:z6NxKNqDVbhS3lyAq2g3XHZhW+/d/DQsnYMBiTN84H0= github.com/xssnick/tonutils-go v1.8.7/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= +github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7 h1:VNEBUjuPRk8pba7TR27nBI1YHF3oqrxBNEl2/ux0Yms= +github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= From 6392f26410ed1a6083337f42b913c5f1170b2244 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Dec 2023 13:57:22 +0530 Subject: [PATCH 042/186] [fetcher] account libraries: fix code style, do not serialize publishers field in shared_lib_descr, add tests --- internal/app/fetcher/account.go | 7 ++- internal/app/fetcher/cache.go | 8 +-- internal/app/fetcher/fetcher_test.go | 8 ++- internal/app/fetcher/libraries.go | 75 +++++++++++------------ internal/app/fetcher/libraries_test.go | 85 ++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 47 deletions(-) create mode 100644 internal/app/fetcher/libraries_test.go diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index f833e05d..205308d0 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -2,13 +2,14 @@ package fetcher import ( "context" - "github.com/tonindexer/anton/abi" - "github.com/xssnick/tonutils-go/tvm/cell" "time" "github.com/pkg/errors" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" @@ -34,7 +35,7 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr acc = MapAccount(b, raw) if raw.Code != nil { - libs, err := s.GetAccountLibraries(ctx, raw) + libs, err := s.getAccountLibraries(ctx, raw) if err != nil { return nil, errors.Wrapf(err, "get account libraries") } diff --git a/internal/app/fetcher/cache.go b/internal/app/fetcher/cache.go index 2be166cb..454e4ffb 100644 --- a/internal/app/fetcher/cache.go +++ b/internal/app/fetcher/cache.go @@ -124,17 +124,17 @@ func (c *accountCache) set(bExt *ton.BlockIDExt, acc *core.AccountState) { } type librariesCache struct { - libs map[string]*LibDescription + libs map[string]*libDescription sync.Mutex } func newLibrariesCache() *librariesCache { return &librariesCache{ - libs: map[string]*LibDescription{}, + libs: map[string]*libDescription{}, } } -func (c *librariesCache) get(hash []byte) *LibDescription { +func (c *librariesCache) get(hash []byte) *libDescription { c.Lock() defer c.Unlock() @@ -146,7 +146,7 @@ func (c *librariesCache) get(hash []byte) *LibDescription { return nil } -func (c *librariesCache) set(hash []byte, desc *LibDescription) { +func (c *librariesCache) set(hash []byte, desc *libDescription) { c.Lock() defer c.Unlock() diff --git a/internal/app/fetcher/fetcher_test.go b/internal/app/fetcher/fetcher_test.go index e6c6cc47..65241fa4 100644 --- a/internal/app/fetcher/fetcher_test.go +++ b/internal/app/fetcher/fetcher_test.go @@ -16,10 +16,14 @@ import ( "github.com/tonindexer/anton/internal/app/parser" ) -var bcConfig *cell.Cell +const bcConfigBase64 = "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" + +var ( + bcConfig *cell.Cell +) func init() { - bcConfigBOC, err := base64.StdEncoding.DecodeString("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") + bcConfigBOC, err := base64.StdEncoding.DecodeString(bcConfigBase64) if err != nil { panic(err) } diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index edd7aed3..67662f77 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -2,52 +2,19 @@ package fetcher import ( "context" + "github.com/pkg/errors" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" ) -type LibDescription struct { - _ tlb.Magic `tlb:"$00"` - Lib *cell.Cell `tlb:"^"` - Publishers *cell.Dictionary `tlb:"dict inline 256"` -} - -func (s *Service) GetAccountLibraries(ctx context.Context, raw *tlb.Account) (*cell.Cell, error) { - hashes, err := findLibraries(raw.Code) - if err != nil { - return nil, errors.Wrapf(err, "find libraries") - } - - libs, err := s.API.GetLibraries(ctx, hashes...) - if err != nil { - return nil, errors.Wrapf(err, "get libraries") - } - - libsMap := cell.NewDict(256) - - for i, hash := range hashes { - desc := LibDescription{Lib: libs[i]} - - h := cell.BeginCell().MustStoreSlice(hash, 256).EndCell() - t, err := tlb.ToCell(desc) - if err != nil { - return nil, err - } - - if err = libsMap.Set(h, t); err != nil { - return nil, err - } - - s.libraries.set(hash, &desc) - } - - return libsMap.ToCell() +type libDescription struct { + _ tlb.Magic `tlb:"$00"` + Lib *cell.Cell `tlb:"^"` } func getLibraryHash(code *cell.Cell) ([]byte, error) { hash, err := code.BeginParse().LoadBinarySnake() - if err != nil { return nil, err } @@ -60,7 +27,6 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { if code.GetType() == cell.LibraryCellType { hash, err := getLibraryHash(code) - if err != nil { return nil, err } @@ -90,3 +56,36 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { return hashes, nil } + +func (s *Service) getAccountLibraries(ctx context.Context, raw *tlb.Account) (*cell.Cell, error) { + hashes, err := findLibraries(raw.Code) + if err != nil { + return nil, errors.Wrapf(err, "find libraries") + } + + libs, err := s.API.GetLibraries(ctx, hashes...) + if err != nil { + return nil, errors.Wrapf(err, "get libraries") + } + + libsMap := cell.NewDict(256) + + for i, hash := range hashes { + desc := libDescription{Lib: libs[i]} + + t, err := tlb.ToCell(&desc) + if err != nil { + return nil, err + } + + h := cell.BeginCell().MustStoreSlice(hash, 256).EndCell() + + if err = libsMap.Set(h, t); err != nil { + return nil, err + } + + s.libraries.set(hash, &desc) + } + + return libsMap.ToCell() +} diff --git a/internal/app/fetcher/libraries_test.go b/internal/app/fetcher/libraries_test.go new file mode 100644 index 00000000..f5ee248a --- /dev/null +++ b/internal/app/fetcher/libraries_test.go @@ -0,0 +1,85 @@ +package fetcher + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" +) + +func TestService_getAccountLibraries(t *testing.T) { + s := newService(t) + + ctx := context.Background() + + m, err := s.API.GetMasterchainInfo(ctx) + require.NoError(t, err) + + addresses := []string{ + "EQBQ1N2kXSnH1Kn-Ds9hAb975AH4ONhnQQKFxoVJQcjk-R3d", + "EQA4p5Xw8jOXwZFq2bg50b68E8lQTSHjnorzb6eu7-Bi7Vf5", + "EQAOtILmq55sXyBVzZcIbT8k_llRMX4cB4_CqHoOXGI5Pt_k", + "EQDK-nuKv1Rvb9YeV84e707l0FaLujTsj-TMsrQ1jX1M71Id", + "EQAtxlncJm6z8o9lEVDUzdhQ3xFlFnsQwoUInnlTFccbU1KN", + "EQDynReiCeK8xlKRbYArpp4jyzZuF6-tYfhFM0O5ulOs5H0L", + } + + for _, addrStr := range addresses { + a := addr.MustFromBase64(addrStr) + + raw, err := s.API.GetAccount(ctx, m, a.MustToTonutils()) + require.NoError(t, err) + + _, err = s.getAccountLibraries(ctx, raw) + require.NoError(t, err) + } +} + +func TestService_getAccountLibraries_emulate(t *testing.T) { + s := newService(t) + + ctx := context.Background() + + m, err := s.API.GetMasterchainInfo(ctx) + require.NoError(t, err) + + addrStr := "0:38a795f0f23397c1916ad9b839d1bebc13c9504d21e39e8af36fa7aeefe062ed" + + a := addr.MustFromString(addrStr) + + raw, err := s.API.GetAccount(ctx, m, a.MustToTonutils()) + require.NoError(t, err) + + acc := MapAccount(m, raw) + + lib, err := s.getAccountLibraries(ctx, raw) + require.NoError(t, err) + + acc.Libraries = lib.ToBOC() + + codeBase64, dataBase64, librariesBase64 := + base64.StdEncoding.EncodeToString(acc.Code), + base64.StdEncoding.EncodeToString(acc.Data), + base64.StdEncoding.EncodeToString(acc.Libraries) + + e, err := abi.NewEmulatorBase64(acc.Address.MustToTonutils(), codeBase64, dataBase64, bcConfigBase64, librariesBase64) + require.NoError(t, err) + + retValues := []abi.VmValueDesc{ + { + Name: "contract_data", + StackType: abi.VmCell, + }, + } + + retStack, err := e.RunGetMethod(ctx, "get_position_manager_contract_data", nil, retValues) + require.NoError(t, err) + require.Equal(t, 1, len(retStack)) + + t.Logf("%x", retStack[0].Payload.(*cell.Cell).ToBOC()) +} From cda00c77e896fc4b7d545fa25859149b38a6c7e8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 6 Feb 2024 19:10:33 +0700 Subject: [PATCH 043/186] [fetcher] getAccount: fix nil dereference on library get method hashes --- internal/app/fetcher/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 205308d0..fb05c768 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -51,7 +51,7 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr } lib := s.libraries.get(hash) - if lib != nil { + if lib != nil && lib.Lib != nil { acc.GetMethodHashes, _ = abi.GetMethodHashes(lib.Lib) } } else { From bf9546fc514bb2e9106f06bd9008a9e6cdde1e44 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 17:47:36 +0700 Subject: [PATCH 044/186] [fetcher] getAccount: get account state from the database on getOtherAccount --- cmd/indexer/indexer.go | 6 ++- internal/app/fetcher.go | 5 +- internal/app/fetcher/account.go | 58 +++++++++++++++++++--- internal/app/fetcher/fetcher_test.go | 4 +- internal/app/fetcher/tx.go | 12 ++--- internal/app/indexer/fetch.go | 4 +- internal/core/filter/account.go | 5 ++ internal/core/repository/account/filter.go | 22 ++++++++ 8 files changed, 97 insertions(+), 19 deletions(-) diff --git a/cmd/indexer/indexer.go b/cmd/indexer/indexer.go index e3000e8b..a649c169 100644 --- a/cmd/indexer/indexer.go +++ b/cmd/indexer/indexer.go @@ -19,6 +19,7 @@ import ( "github.com/tonindexer/anton/internal/app/indexer" "github.com/tonindexer/anton/internal/app/parser" "github.com/tonindexer/anton/internal/core/repository" + "github.com/tonindexer/anton/internal/core/repository/account" "github.com/tonindexer/anton/internal/core/repository/contract" ) @@ -77,8 +78,9 @@ var Command = &cli.Command{ ContractRepo: contractRepo, }) f := fetcher.NewService(&app.FetcherConfig{ - API: api, - Parser: p, + API: api, + AccountRepo: account.NewRepository(conn.CH, conn.PG), + Parser: p, }) i := indexer.NewService(&app.IndexerConfig{ DB: conn, diff --git a/internal/app/fetcher.go b/internal/app/fetcher.go index a316978e..f4ddadf7 100644 --- a/internal/app/fetcher.go +++ b/internal/app/fetcher.go @@ -9,11 +9,14 @@ import ( "github.com/xssnick/tonutils-go/ton" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" ) type FetcherConfig struct { API ton.APIClientWrapped + AccountRepo filter.AccountRepository + Parser ParserService } @@ -29,5 +32,5 @@ type FetcherService interface { LookupMaster(ctx context.Context, api ton.APIClientWrapped, seqNo uint32) (*ton.BlockIDExt, error) UnseenBlocks(ctx context.Context, masterSeqNo uint32) (master *ton.BlockIDExt, shards []*ton.BlockIDExt, err error) UnseenShards(ctx context.Context, master *ton.BlockIDExt) (shards []*ton.BlockIDExt, err error) - BlockTransactions(ctx context.Context, b *ton.BlockIDExt) ([]*core.Transaction, error) + BlockTransactions(ctx context.Context, master, b *ton.BlockIDExt) ([]*core.Transaction, error) } diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index fb05c768..64e57977 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -5,7 +5,7 @@ import ( "time" "github.com/pkg/errors" - + "github.com/rs/zerolog/log" "github.com/xssnick/tonutils-go/ton" "github.com/xssnick/tonutils-go/tvm/cell" @@ -13,9 +13,42 @@ import ( "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" ) -func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { +func (s *Service) getLastSeenAccountState(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { + defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) + + var latestBlock *core.BlockID + switch { + case int8(b.Workchain) == a.Workchain(): + latestBlock = &core.BlockID{Workchain: b.Workchain, Shard: b.Shard, SeqNo: b.SeqNo} + case int8(master.Workchain) == a.Workchain(): + latestBlock = &core.BlockID{Workchain: master.Workchain, Shard: master.Shard, SeqNo: master.SeqNo} + default: + return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) + } + + accountReq := filter.AccountsReq{ + Addresses: []*addr.Address{&a}, + Workchain: &latestBlock.Workchain, + Shard: &latestBlock.Shard, + BlockSeqNoLeq: &latestBlock.SeqNo, + Order: "DESC", + Limit: 1, + } + accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) + if err != nil { + return nil, errors.Wrap(err, "filter accounts") + } + if len(accountRes.Rows) < 1 { + return nil, errors.Wrap(core.ErrNotFound, "could not find needed account state") + } + + return accountRes.Rows[0], nil +} + +func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { acc, ok := s.accounts.get(b, a) if ok { return acc, nil @@ -25,7 +58,7 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr return nil, errors.Wrap(core.ErrNotFound, "skip account") } - defer app.TimeTrack(time.Now(), "getAccount(%d, %d, %s)", b.Workchain, b.SeqNo, a.String()) + defer app.TimeTrack(time.Now(), "getAccount(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) if err != nil { @@ -71,11 +104,24 @@ func (s *Service) getAccount(ctx context.Context, b *ton.BlockIDExt, a addr.Addr if ok { return acc, nil } - raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) + + // second attempt is to look for the latest account state in the database + acc, err := s.getLastSeenAccountState(ctx, master, b, a) if err == nil { - return MapAccount(b, raw), nil + return acc, nil + } + lvl := log.Warn() + if errors.Is(err, core.ErrNotFound) || errors.Is(err, core.ErrInvalidArg) { + lvl = log.Debug() + } + lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") + + // third attempt is to get needed contract state from the node + raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) + if err != nil { + return nil, errors.Wrapf(err, "cannot get %s account state", a.Base64()) } - return nil, errors.Wrapf(core.ErrNotFound, "cannot find %s account state", a.Base64()) + return MapAccount(b, raw), nil } err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) diff --git a/internal/app/fetcher/fetcher_test.go b/internal/app/fetcher/fetcher_test.go index 65241fa4..c9cd9e14 100644 --- a/internal/app/fetcher/fetcher_test.go +++ b/internal/app/fetcher/fetcher_test.go @@ -71,14 +71,14 @@ func TestService_BlockTransactions(t *testing.T) { go func() { defer wg.Done() - _, err = s.BlockTransactions(ctx, master) + _, err = s.BlockTransactions(ctx, master, master) require.Nil(t, err) }() for i := range shards { go func(shard *ton.BlockIDExt) { defer wg.Done() - _, err := s.BlockTransactions(ctx, shard) + _, err := s.BlockTransactions(ctx, master, shard) require.Nil(t, err) }(shards[i]) } diff --git a/internal/app/fetcher/tx.go b/internal/app/fetcher/tx.go index aefe2726..70da748a 100644 --- a/internal/app/fetcher/tx.go +++ b/internal/app/fetcher/tx.go @@ -14,7 +14,7 @@ import ( "github.com/tonindexer/anton/internal/core" ) -func (s *Service) getTransaction(ctx context.Context, b *ton.BlockIDExt, id ton.TransactionShortInfo) (*core.Transaction, error) { +func (s *Service) getTransaction(ctx context.Context, master, b *ton.BlockIDExt, id ton.TransactionShortInfo) (*core.Transaction, error) { var tx *core.Transaction var acc *core.AccountState @@ -47,7 +47,7 @@ func (s *Service) getTransaction(ctx context.Context, b *ton.BlockIDExt, id ton. accCh := make(chan ret) go func() { - acc, err := s.getAccount(ctx, b, *addr.MustFromTonutils(a)) + acc, err := s.getAccount(ctx, master, b, *addr.MustFromTonutils(a)) accCh <- ret{res: acc, err: errors.Wrapf(err, "get account (addr = %s)", a)} }() @@ -88,7 +88,7 @@ func (s *Service) getTransaction(ctx context.Context, b *ton.BlockIDExt, id ton. return tx, nil } -func (s *Service) getTransactions(ctx context.Context, b *ton.BlockIDExt, ids []ton.TransactionShortInfo) ([]*core.Transaction, error) { +func (s *Service) getTransactions(ctx context.Context, master, b *ton.BlockIDExt, ids []ton.TransactionShortInfo) ([]*core.Transaction, error) { var wg sync.WaitGroup type ret struct { @@ -105,7 +105,7 @@ func (s *Service) getTransactions(ctx context.Context, b *ton.BlockIDExt, ids [] for i := range ids { go func(id ton.TransactionShortInfo) { defer wg.Done() - tx, err := s.getTransaction(ctx, b, id) + tx, err := s.getTransaction(ctx, master, b, id) ch <- ret{tx: tx, err: err} }(ids[i]) } @@ -133,7 +133,7 @@ func (s *Service) fetchTxIDs(ctx context.Context, b *ton.BlockIDExt, after *ton. return s.API.GetBlockTransactionsV2(ctx, b, 100, after) } -func (s *Service) BlockTransactions(ctx context.Context, b *ton.BlockIDExt) ([]*core.Transaction, error) { +func (s *Service) BlockTransactions(ctx context.Context, master, b *ton.BlockIDExt) ([]*core.Transaction, error) { var ( after *ton.TransactionID3 fetchedIDs []ton.TransactionShortInfo @@ -153,7 +153,7 @@ func (s *Service) BlockTransactions(ctx context.Context, b *ton.BlockIDExt) ([]* after = fetchedIDs[len(fetchedIDs)-1].ID3() } - rawTx, err := s.getTransactions(ctx, b, fetchedIDs) + rawTx, err := s.getTransactions(ctx, master, b, fetchedIDs) if err != nil { return nil, err } diff --git a/internal/app/indexer/fetch.go b/internal/app/indexer/fetch.go index 5c6be4fe..bf773b51 100644 --- a/internal/app/indexer/fetch.go +++ b/internal/app/indexer/fetch.go @@ -63,7 +63,7 @@ func (s *Service) fetchMaster(seq uint32) *core.Block { go func() { defer wg.Done() - tx, err := s.Fetcher.BlockTransactions(ctx, master) + tx, err := s.Fetcher.BlockTransactions(ctx, master, master) ch <- processedBlock{ block: &core.Block{ @@ -83,7 +83,7 @@ func (s *Service) fetchMaster(seq uint32) *core.Block { go func(shard *ton.BlockIDExt) { defer wg.Done() - tx, err := s.Fetcher.BlockTransactions(ctx, shard) + tx, err := s.Fetcher.BlockTransactions(ctx, master, shard) ch <- processedBlock{ block: &core.Block{ diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index 087d249c..7d4476bb 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -24,6 +24,11 @@ type AccountsReq struct { Addresses []*addr.Address // `form:"addresses"` LatestState bool `form:"latest"` + // filter by block + Workchain *int32 + Shard *int64 + BlockSeqNoLeq *uint32 + // contract data filter ContractTypes []abi.ContractName `form:"interface"` OwnerAddress *addr.Address // `form:"owner_address"` diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index e433d32c..98b8c00f 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -103,6 +103,17 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts if len(f.Addresses) > 0 { q = q.Where(statesTable+"address in (?)", bun.In(f.Addresses)) } + + if f.Workchain != nil { + q = q.Where(prefix+"workchain = ?", *f.Workchain) + } + if f.Shard != nil { + q = q.Where(prefix+"shard = ?", *f.Shard) + } + if f.BlockSeqNoLeq != nil { + q = q.Where(prefix+"block_seq_no <= ?", *f.BlockSeqNoLeq) + } + if len(f.ContractTypes) > 0 { q = q.Where(prefix+"types && ?", pgdialect.Array(f.ContractTypes)) } @@ -154,6 +165,17 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR if len(f.Addresses) > 0 { q = q.Where("address in (?)", ch.In(f.Addresses)) } + + if f.Workchain != nil { + q = q.Where("workchain = ?", *f.Workchain) + } + if f.Shard != nil { + q = q.Where("shard = ?", *f.Shard) + } + if f.BlockSeqNoLeq != nil { + q = q.Where("block_seq_no <= ?", *f.BlockSeqNoLeq) + } + if len(f.ContractTypes) > 0 { q = q.Where("hasAny(types, [?])", ch.In(f.ContractTypes)) } From 65d93efb2e56181df18a67aae68a37491bd71d1e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:09:13 +0700 Subject: [PATCH 045/186] [lint] gocyclo min-complexity: 18 --- .golangci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yaml b/.golangci.yaml index a16e7933..8f1df8f9 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -55,7 +55,7 @@ linters: - whitespace linters-settings: gocyclo: - min-complexity: 17 + min-complexity: 18 gosec: excludes: - G404 From f317c7cd3743da452cf0ca8f035c2f62317623fb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:10:06 +0700 Subject: [PATCH 046/186] [fetcher] getLastSeenAccountState: fix int truncation in comparison --- internal/app/fetcher/account.go | 62 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 64e57977..1d0b0714 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -21,9 +21,9 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, master, b *ton.Bl var latestBlock *core.BlockID switch { - case int8(b.Workchain) == a.Workchain(): + case b.Workchain == int32(a.Workchain()): latestBlock = &core.BlockID{Workchain: b.Workchain, Shard: b.Shard, SeqNo: b.SeqNo} - case int8(master.Workchain) == a.Workchain(): + case master.Workchain == int32(a.Workchain()): latestBlock = &core.BlockID{Workchain: master.Workchain, Shard: master.Shard, SeqNo: master.SeqNo} default: return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) @@ -48,6 +48,35 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, master, b *ton.Bl return accountRes.Rows[0], nil } +func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + // first attempt is to look for an account in this given block + acc, ok := s.accounts.get(b, a) + if ok { + return acc, nil + } + + // second attempt is to look for the latest account state in the database + acc, err := s.getLastSeenAccountState(ctx, master, b, a) + if err == nil { + return acc, nil + } + lvl := log.Warn() + if errors.Is(err, core.ErrNotFound) || errors.Is(err, core.ErrInvalidArg) { + lvl = log.Debug() + } + lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") + + // third attempt is to get needed contract state from the node + raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) + if err != nil { + return nil, errors.Wrapf(err, "cannot get %s account state", a.Base64()) + } + return MapAccount(b, raw), nil + } + return getOtherAccountFunc +} + func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { acc, ok := s.accounts.get(b, a) if ok { @@ -67,12 +96,11 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a acc = MapAccount(b, raw) - if raw.Code != nil { + if raw.Code != nil { //nolint:nestif // getting get method hashes from the library libs, err := s.getAccountLibraries(ctx, raw) if err != nil { return nil, errors.Wrapf(err, "get account libraries") } - if libs != nil { acc.Libraries = libs.ToBOC() } @@ -98,31 +126,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a // sometimes, to parse the full account data we need to get other contracts states // for example, to get nft item data - getOtherAccount := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - // first attempt is to look for an account in this given block - acc, ok := s.accounts.get(b, a) - if ok { - return acc, nil - } - - // second attempt is to look for the latest account state in the database - acc, err := s.getLastSeenAccountState(ctx, master, b, a) - if err == nil { - return acc, nil - } - lvl := log.Warn() - if errors.Is(err, core.ErrNotFound) || errors.Is(err, core.ErrInvalidArg) { - lvl = log.Debug() - } - lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") - - // third attempt is to get needed contract state from the node - raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) - if err != nil { - return nil, errors.Wrapf(err, "cannot get %s account state", a.Base64()) - } - return MapAccount(b, raw), nil - } + getOtherAccount := s.makeGetOtherAccountFunc(master, b) err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { From 1b5328feaca136c12052721ca38a48eb1fb376eb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:10:22 +0700 Subject: [PATCH 047/186] [addr] UnmarshalJSON: fix style --- addr/address.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addr/address.go b/addr/address.go index 6a59f211..e29eb263 100644 --- a/addr/address.go +++ b/addr/address.go @@ -142,7 +142,7 @@ func (x *Address) MarshalJSON() ([]byte, error) { func (x *Address) UnmarshalJSON(raw []byte) error { s := strings.Replace(string(raw), "\"", "", 2) s = strings.TrimSpace(s) - if len(s) > 0 && s[0] == '{' { + if s != "" && s[0] == '{' { var bothAddr struct { Hex string `json:"hex"` Base64 string `json:"base64"` From df74d30f88ee95804971d683b284a58206830972 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:10:58 +0700 Subject: [PATCH 048/186] [abi] get_emulator.go: fix goimports --- abi/get_emulator.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index df53681a..e2851b5f 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -4,9 +4,10 @@ import ( "context" "encoding/base64" "fmt" + "math/big" + "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/txemulator" - "math/big" "github.com/pkg/errors" From 2cc1ce2b1c4d88555873ca25a438394e7492f0d2 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:11:32 +0700 Subject: [PATCH 049/186] [fetcher] findLibraries: fix loop uint iterator --- internal/app/fetcher/libraries.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/fetcher/libraries.go b/internal/app/fetcher/libraries.go index 67662f77..4b692494 100644 --- a/internal/app/fetcher/libraries.go +++ b/internal/app/fetcher/libraries.go @@ -40,7 +40,7 @@ func findLibraries(code *cell.Cell) ([][]byte, error) { return hashes, nil } - for i := code.RefsNum(); i < 0; i-- { + for i := code.RefsNum(); i < 1; i-- { ref, err := code.PeekRef(int(i - 1)) if err != nil { return nil, err @@ -80,7 +80,7 @@ func (s *Service) getAccountLibraries(ctx context.Context, raw *tlb.Account) (*c h := cell.BeginCell().MustStoreSlice(hash, 256).EndCell() - if err = libsMap.Set(h, t); err != nil { + if err := libsMap.Set(h, t); err != nil { return nil, err } From 1050a0750e1e72667ccdebc2a7c54994dcf078b1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 12 Feb 2024 18:12:09 +0700 Subject: [PATCH 050/186] [fetcher] getAccountLibraries_emulate test: fix forcetypeassert lint --- internal/app/fetcher/libraries_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/app/fetcher/libraries_test.go b/internal/app/fetcher/libraries_test.go index f5ee248a..2a5fb987 100644 --- a/internal/app/fetcher/libraries_test.go +++ b/internal/app/fetcher/libraries_test.go @@ -81,5 +81,7 @@ func TestService_getAccountLibraries_emulate(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(retStack)) - t.Logf("%x", retStack[0].Payload.(*cell.Cell).ToBOC()) + c, ok := retStack[0].Payload.(*cell.Cell) + require.True(t, ok) + t.Logf("%x", c.ToBOC()) } From e258f9d001bff8a02bfdd0a01a0d23394fb13adf Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 18:38:29 +0700 Subject: [PATCH 051/186] [parser] ParseAccountData: zero out old account interfaces --- internal/app/parser/account.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 1d10e9ed..f60dce46 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -111,6 +111,7 @@ func (s *Service) ParseAccountData( return errors.Wrap(app.ErrImpossibleParsing, "unknown contract interfaces") } + acc.Types = nil for _, i := range interfaces { acc.Types = append(acc.Types, i.Name) } From 5a2a3ed292c71487782ad2fb300c4d1649fa39fe Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 18:39:14 +0700 Subject: [PATCH 052/186] [rndm] AddressState: generate random libraries field --- internal/core/rndm/account.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/core/rndm/account.go b/internal/core/rndm/account.go index c0103a0d..330a146b 100644 --- a/internal/core/rndm/account.go +++ b/internal/core/rndm/account.go @@ -52,6 +52,7 @@ func AddressState(a *addr.Address, t []abi.ContractName, minter *addr.Address) * CodeHash: Bytes(32), Data: Bytes(32), DataHash: Bytes(32), + Libraries: Bytes(32), GetMethodHashes: GetMethodHashes(), Types: t, OwnerAddress: Address(), From 57e672189cd69163cede713fc42ac6bf7543aed2 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 18:40:22 +0700 Subject: [PATCH 053/186] [core] account repo: method for updating account states --- internal/core/account.go | 1 + internal/core/repository/account/account.go | 49 +++++++++++++++++++ .../core/repository/account/account_test.go | 21 ++++++++ 3 files changed, 71 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index 78bb8acb..7188e30f 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -135,4 +135,5 @@ type AccountRepository interface { GetAddressLabel(context.Context, addr.Address) (*AddressLabel, error) AddAccountStates(ctx context.Context, tx bun.Tx, states []*AccountState) error + UpdateAccountStates(ctx context.Context, states []*AccountState) error } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 75a4380a..064ea0dd 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -3,9 +3,11 @@ package account import ( "context" "database/sql" + "encoding/json" "strings" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" @@ -228,3 +230,50 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ return nil } + +func logAccountStateDataUpdate(acc *core.AccountState) { + types, _ := json.Marshal(acc.Types) //nolint:errchkjson // no need + getMethods, _ := json.Marshal(acc.ExecutedGetMethods) //nolint:errchkjson // no need + + log.Info(). + Str("address", acc.Address.Base64()). + Uint64("last_tx_lt", acc.LastTxLT). + RawJSON("types", types). + RawJSON("executed_get_methods", getMethods). + Msg("updating account state data") +} + +func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.AccountState) error { + if len(accounts) == 0 { + return nil + } + + for _, a := range accounts { + logAccountStateDataUpdate(a) + + _, err := r.pg.NewUpdate().Model(a). + Set("types = ?types"). + Set("owner_address = ?owner_address"). + Set("minter_address = ?minter_address"). + Set("fake = ?fake"). + Set("executed_get_methods = ?executed_get_methods"). + Set("content_uri = ?content_uri"). + Set("content_name = ?content_name"). + Set("content_description = ?content_description"). + Set("content_image = ?content_image"). + Set("content_image_data = ?content_image_data"). + Set("jetton_balance = ?jetton_balance"). + WherePK(). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, "cannot update %s acc state data", a.Address.String()) + } + } + + _, err := r.ch.NewInsert().Model(&accounts).Exec(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index da8d87e7..e2de09e7 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -13,6 +13,7 @@ import ( "github.com/uptrace/bun/driver/pgdriver" "github.com/uptrace/go-clickhouse/ch" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository/account" "github.com/tonindexer/anton/internal/core/rndm" @@ -111,6 +112,26 @@ func TestRepository_AddAccounts(t *testing.T) { require.Nil(t, err) }) + t.Run("update account state", func(t *testing.T) { + states[0].ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{ + "nft_item": {{Name: "shiny_method", Error: "failed"}}, + } + + err = repo.UpdateAccountStates(ctx, []*core.AccountState{states[0]}) + require.Nil(t, err) + + got := new(core.AccountState) + + err = pg.NewSelect().Model(got).Where("address = ?", a).Where("last_tx_lt = ?", states[0].LastTxLT).Scan(ctx) + require.Nil(t, err) + require.Equal(t, states[0], got) + + err = ck.NewSelect().Model(got).Where("address = ?", a).Where("last_tx_lt = ?", states[0].LastTxLT).Scan(ctx) + require.Nil(t, err) + got.UpdatedAt = states[0].UpdatedAt // TODO: look at time.Time ch unmarshal + require.Equal(t, states[0], got) + }) + t.Run("drop tables again", func(t *testing.T) { dropTables(t) }) From 7e211bf2aa147a0ff7ff788f438439e72bca3928 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 18:41:17 +0700 Subject: [PATCH 054/186] [core] account repo: add filter for bigger or equal block sequence number --- internal/core/filter/account.go | 1 + internal/core/repository/account/filter.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index 7d4476bb..797b16e9 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -28,6 +28,7 @@ type AccountsReq struct { Workchain *int32 Shard *int64 BlockSeqNoLeq *uint32 + BlockSeqNoBeq *uint32 // contract data filter ContractTypes []abi.ContractName `form:"interface"` diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 98b8c00f..700598bf 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -175,6 +175,9 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR if f.BlockSeqNoLeq != nil { q = q.Where("block_seq_no <= ?", *f.BlockSeqNoLeq) } + if f.BlockSeqNoBeq != nil { + q = q.Where("block_seq_no >= ?", *f.BlockSeqNoBeq) + } if len(f.ContractTypes) > 0 { q = q.Where("hasAny(types, [?])", ch.In(f.ContractTypes)) From 82544a73aa49a8324d2454b7046756a54c78b859 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 19:05:28 +0700 Subject: [PATCH 055/186] [core] message repo: method for updating message data --- internal/core/msg.go | 2 ++ internal/core/repository/msg/msg.go | 40 ++++++++++++++++++++++++ internal/core/repository/msg/msg_test.go | 19 +++++++++++ 3 files changed, 61 insertions(+) diff --git a/internal/core/msg.go b/internal/core/msg.go index 2fa428b3..ec7bf492 100644 --- a/internal/core/msg.go +++ b/internal/core/msg.go @@ -79,5 +79,7 @@ type Message struct { type MessageRepository interface { AddMessages(ctx context.Context, tx bun.Tx, messages []*Message) error + UpdateMessages(ctx context.Context, messages []*Message) error + GetMessage(ctx context.Context, hash []byte) (*Message, error) } diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 75cad7c7..35bea58a 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" @@ -199,6 +200,45 @@ func (r *Repository) AddMessages(ctx context.Context, tx bun.Tx, messages []*cor return nil } +func (r *Repository) UpdateMessages(ctx context.Context, messages []*core.Message) error { + if len(messages) == 0 { + return nil + } + + for _, msg := range messages { + log.Info(). + Hex("msg_hash", msg.Hash). + Str("src_address", msg.SrcAddress.Base64()). + Str("src_contract", string(msg.SrcContract)). + Str("dst_address", msg.DstAddress.Base64()). + Str("dst_contract", string(msg.DstContract)). + Uint32("operation_id", msg.OperationID). + Str("operation_name", msg.OperationName). + RawJSON("data_json", msg.DataJSON). + Str("error", msg.Error). + Msg("updating message") + + _, err := r.pg.NewUpdate().Model(msg). + Set("src_contract = ?src_contract"). + Set("dst_contract = ?dst_contract"). + Set("operation_name = ?operation_name"). + Set("data_json = ?data_json"). + Set("error = ?error"). + WherePK(). + Exec(ctx) + if err != nil { + return err + } + } + + _, err := r.ch.NewInsert().Model(&messages).Exec(ctx) + if err != nil { + return err + } + + return nil +} + func (r *Repository) GetMessage(ctx context.Context, hash []byte) (*core.Message, error) { var ret core.Message diff --git a/internal/core/repository/msg/msg_test.go b/internal/core/repository/msg/msg_test.go index 07a4c9f0..ffbf540f 100644 --- a/internal/core/repository/msg/msg_test.go +++ b/internal/core/repository/msg/msg_test.go @@ -102,4 +102,23 @@ func TestRepository_AddMessages(t *testing.T) { err := tx.Commit() require.Nil(t, err) }) + + t.Run("update message", func(t *testing.T) { + messages[0].SrcContract, messages[0].DstContract = "11", "22" + messages[0].DataJSON = []byte(`{"qwerty": ["poiuytr"]}`) + + err := repo.UpdateMessages(ctx, []*core.Message{messages[0]}) + require.Nil(t, err) + + got := new(core.Message) + + err = pg.NewSelect().Model(got).Where("hash = ?", messages[0].Hash).Scan(ctx) + require.Nil(t, err) + require.Equal(t, messages[0], got) + + err = ck.NewSelect().Model(got).Where("hash = ?", messages[0].Hash).Scan(ctx) + require.Nil(t, err) + got.CreatedAt = messages[0].CreatedAt // TODO: look at time.Time ch unmarshal + require.Equal(t, messages[0], got) + }) } From d692254b63d82ac4ba9a52f0020b7f3c0aaae7b7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 20:09:32 +0700 Subject: [PATCH 056/186] [core] contract repo: add table for rescan tasks --- internal/app/parser/parser_test.go | 9 +++ internal/core/contract.go | 15 ++++ internal/core/errors.go | 6 +- internal/core/repository/contract/contract.go | 81 +++++++++++++++++++ .../core/repository/contract/contract_test.go | 79 +++++++++++++++++- .../20240213085742_reindex_state.down.sql | 0 .../20240213085742_reindex_state.up.sql | 0 .../20240213085742_reindex_state.down.sql | 3 + .../20240213085742_reindex_state.up.sql | 18 +++++ 9 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 migrations/chmigrations/20240213085742_reindex_state.down.sql create mode 100644 migrations/chmigrations/20240213085742_reindex_state.up.sql create mode 100644 migrations/pgmigrations/20240213085742_reindex_state.down.sql create mode 100644 migrations/pgmigrations/20240213085742_reindex_state.up.sql diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index 30403ed3..adf8b913 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/uptrace/bun" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" @@ -62,6 +63,14 @@ func (m *mockContractRepo) GetOperationByID(_ context.Context, _ core.MessageTyp panic("implement me") } +func (m *mockContractRepo) GetUnfinishedRescanTask(_ context.Context) (bun.Tx, *core.RescanTask, error) { + panic("implement me") +} + +func (m *mockContractRepo) SetRescanTask(_ context.Context, _ bun.Tx, _ *core.RescanTask) error { + panic("implement me") +} + func newService(t *testing.T) *Service { walletV3R2Code, err := base64.StdEncoding.DecodeString("te6cckEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVBC9ba0=") require.Nil(t, err) diff --git a/internal/core/contract.go b/internal/core/contract.go index e4ced4fb..9f6316af 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -38,6 +38,18 @@ type ContractOperation struct { Schema abi.OperationDesc `bun:"type:jsonb" json:"schema"` } +type RescanTask struct { + bun.BaseModel `bun:"table:rescan_tasks" json:"-"` + + ID int `bun:",pk,autoincrement"` + Finished bool `bun:"finished,notnull"` + StartFrom uint32 `bun:"start_from_start_from_masterchain_seq_nomasterchain_seq_no,notnull"` + AccountsLastMaster uint32 `bun:"accounts_last_masterchain_seq_no,notnull"` + AccountsRescanDone bool `bun:",notnull"` + MessagesLastMaster uint32 `bun:"messages_last_masterchain_seq_no,notnull"` + MessagesRescanDone bool `bun:",notnull"` +} + type ContractRepository interface { AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) @@ -50,4 +62,7 @@ type ContractRepository interface { AddOperation(context.Context, *ContractOperation) error GetOperations(context.Context) ([]*ContractOperation, error) GetOperationByID(context.Context, MessageType, []abi.ContractName, bool, uint32) (*ContractOperation, error) + + GetUnfinishedRescanTask(context.Context) (bun.Tx, *RescanTask, error) + SetRescanTask(context.Context, bun.Tx, *RescanTask) error } diff --git a/internal/core/errors.go b/internal/core/errors.go index 9b0078ec..7d16e611 100644 --- a/internal/core/errors.go +++ b/internal/core/errors.go @@ -3,7 +3,7 @@ package core import "errors" var ( - ErrNotFound = errors.New("not found") - ErrInvalidArg = errors.New("invalid arguments") - // ErrNotAvailable = errors.New("not available") + ErrNotFound = errors.New("not found") + ErrInvalidArg = errors.New("invalid arguments") + ErrAlreadyExists = errors.New("already exists") ) diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 92bcf1a7..5643ae13 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -2,6 +2,7 @@ package contract import ( "context" + "database/sql" "strings" "github.com/pkg/errors" @@ -51,6 +52,15 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "contract interface pg create table") } + _, err = pgDB.NewCreateTable(). + Model(&core.RescanTask{}). + IfNotExists(). + WithForeignKeys(). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "rescan task pg create table") + } + _, err = pgDB.NewCreateIndex(). Model(&core.ContractInterface{}). Unique(). @@ -66,6 +76,16 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "messages pg create source tx hash check") } + _, err = pgDB.NewCreateIndex(). + Model(&core.RescanTask{}). + Unique(). + Column("finished"). + Where("finished = false"). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "rescan task finished create unique index") + } + return nil } @@ -228,3 +248,64 @@ func (r *Repository) GetOperationByID(ctx context.Context, t core.MessageType, i return op, nil } + +func (r *Repository) CreateNewRescanTask(ctx context.Context, startFrom uint32) error { + task := core.RescanTask{ + StartFrom: startFrom, + AccountsLastMaster: startFrom - 1, + MessagesLastMaster: startFrom - 1, + } + + _, err := r.pg.NewInsert().Model(&task).Exec(ctx) + if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return errors.Wrap(core.ErrAlreadyExists, "cannot create new task while the previous one is unfinished") + } + return err + } + + return nil +} + +func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core.RescanTask, error) { + var task core.RescanTask + + tx, err := r.pg.Begin() + if err != nil { + return bun.Tx{}, nil, err + } + + err = tx.NewSelect().Model(&task). + Where("finished = ?", false). + For("UPDATE"). + Scan(ctx) + if err != nil { + _ = tx.Rollback() + if errors.Is(err, sql.ErrNoRows) { + return bun.Tx{}, nil, errors.Wrap(core.ErrNotFound, "no unfinished tasks") + } + return bun.Tx{}, nil, err + } + + return tx, &task, nil +} + +func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.RescanTask) error { + _, err := tx.NewUpdate().Model(task). + Set("finished = ?finished"). + Set("accounts_last_masterchain_seq_no = ?accounts_last_masterchain_seq_no"). + Set("accounts_rescan_done = ?accounts_rescan_done"). + Set("messages_last_masterchain_seq_no = ?messages_last_masterchain_seq_no"). + Set("messages_rescan_done = ?messages_rescan_done"). + WherePK(). + Exec(ctx) + if err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 45ab8f02..79e5c0e4 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" @@ -47,7 +48,7 @@ func createTables(t testing.TB) { } func dropTables(t testing.TB) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, err := pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) @@ -56,6 +57,8 @@ func dropTables(t testing.TB) { require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractDefinition)(nil)).IfExists().Exec(ctx) require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.RescanTask)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) _, err = pg.ExecContext(context.Background(), "DROP TYPE message_type") if err != nil && !strings.Contains(err.Error(), "does not exist") { @@ -228,3 +231,77 @@ func TestRepository_AddContracts(t *testing.T) { dropTables(t) }) } + +func TestRepository_CreateNewRescanTask(t *testing.T) { + initdb(t) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + t.Run("drop tables", func(t *testing.T) { + dropTables(t) + }) + + t.Run("create tables", func(t *testing.T) { + createTables(t) + }) + + t.Run("create new task", func(t *testing.T) { + err := repo.CreateNewRescanTask(ctx, 10) + require.NoError(t, err) + }) + + t.Run("get 'already exists' error on second creation of unfinished task", func(t *testing.T) { + err := repo.CreateNewRescanTask(ctx, 10) + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrAlreadyExists)) + }) + + t.Run("update unfinished task", func(t *testing.T) { + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + + task.AccountsRescanDone = true + task.AccountsLastMaster = 30 + task.MessagesLastMaster = 20 + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("finish task", func(t *testing.T) { + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + + task.MessagesRescanDone = true + task.MessagesLastMaster = 30 + task.Finished = true + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("get 'not found' error on choosing unfinished task", func(t *testing.T) { + _, _, err := repo.GetUnfinishedRescanTask(ctx) + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNotFound)) + }) + + t.Run("create second task", func(t *testing.T) { + err := repo.CreateNewRescanTask(ctx, 20) + require.NoError(t, err) + + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + require.Equal(t, 3, task.ID) + + task.Finished = true + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("drop tables", func(t *testing.T) { + dropTables(t) + }) +} diff --git a/migrations/chmigrations/20240213085742_reindex_state.down.sql b/migrations/chmigrations/20240213085742_reindex_state.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/chmigrations/20240213085742_reindex_state.up.sql b/migrations/chmigrations/20240213085742_reindex_state.up.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/pgmigrations/20240213085742_reindex_state.down.sql b/migrations/pgmigrations/20240213085742_reindex_state.down.sql new file mode 100644 index 00000000..f1b4363e --- /dev/null +++ b/migrations/pgmigrations/20240213085742_reindex_state.down.sql @@ -0,0 +1,3 @@ +SET statement_timeout = 0; + +DROP TABLE rescan_tasks; \ No newline at end of file diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql new file mode 100644 index 00000000..0ab13b52 --- /dev/null +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -0,0 +1,18 @@ +SET statement_timeout = 0; + +CREATE SEQUENCE rescan_tasks_id_seq START WITH 1; + +CREATE TABLE rescan_tasks ( + id integer NOT NULL DEFAULT nextval('rescan_tasks_id_seq'), + finished bool NOT NULL, + start_from_masterchain_seq_no integer NOT NULL, + accounts_last_masterchain_seq_no integer NOT NULL, + accounts_rescan_done boolean NOT NULL, + messages_last_masterchain_seq_no integer NOT NULL, + messages_rescan_done boolean NOT NULL, + CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) +); + +ALTER SEQUENCE rescan_tasks_id_seq OWNED BY rescan_tasks.id; + +CREATE UNIQUE INDEX ON rescan_tasks (finished) WHERE finished = false; From 70542a4c354987bbc1693b89640108667f3b24cb Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 20:09:56 +0700 Subject: [PATCH 057/186] [core] message repo: drop tables at the end of AddMessages test --- internal/core/repository/msg/msg_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/core/repository/msg/msg_test.go b/internal/core/repository/msg/msg_test.go index ffbf540f..060dd50f 100644 --- a/internal/core/repository/msg/msg_test.go +++ b/internal/core/repository/msg/msg_test.go @@ -121,4 +121,8 @@ func TestRepository_AddMessages(t *testing.T) { got.CreatedAt = messages[0].CreatedAt // TODO: look at time.Time ch unmarshal require.Equal(t, messages[0], got) }) + + t.Run("drop tables", func(t *testing.T) { + dropTables(t) + }) } From eaaacb2203eb3859c629e42b0ac8153a429ae7e1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 20:57:09 +0700 Subject: [PATCH 058/186] [app] add rescan service --- internal/app/rescan.go | 22 +++++ internal/app/rescan/account.go | 123 ++++++++++++++++++++++++ internal/app/rescan/rescan.go | 170 +++++++++++++++++++++++++++++++++ internal/app/rescan/tx.go | 135 ++++++++++++++++++++++++++ 4 files changed, 450 insertions(+) create mode 100644 internal/app/rescan.go create mode 100644 internal/app/rescan/account.go create mode 100644 internal/app/rescan/rescan.go create mode 100644 internal/app/rescan/tx.go diff --git a/internal/app/rescan.go b/internal/app/rescan.go new file mode 100644 index 00000000..2fe1c811 --- /dev/null +++ b/internal/app/rescan.go @@ -0,0 +1,22 @@ +package app + +import ( + "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/repository" +) + +type RescanConfig struct { + ContractRepo core.ContractRepository + BlockRepo repository.Block + AccountRepo repository.Account + MessageRepo repository.Message + + Parser ParserService + + Workers int +} + +type RescanService interface { + Start() error + Stop() +} diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go new file mode 100644 index 00000000..c92bc421 --- /dev/null +++ b/internal/app/rescan/account.go @@ -0,0 +1,123 @@ +package rescan + +import ( + "context" + "reflect" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" +) + +func (s *Service) getRecentAccountState(ctx context.Context, master, b core.BlockID, a addr.Address, afterBlock bool) (*core.AccountState, error) { + defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) + + var boundBlock core.BlockID + switch { + case b.Workchain == int32(a.Workchain()): + boundBlock = b + case master.Workchain == int32(a.Workchain()): + boundBlock = master + default: + return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) + } + + accountReq := filter.AccountsReq{ + Addresses: []*addr.Address{&a}, + Workchain: &boundBlock.Workchain, + Shard: &boundBlock.Shard, + Order: "DESC", + Limit: 1, + } + if afterBlock { + accountReq.BlockSeqNoBeq = &boundBlock.SeqNo + } else { + accountReq.BlockSeqNoLeq = &boundBlock.SeqNo + } + + accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) + if err != nil { + return nil, errors.Wrap(err, "filter accounts") + } + if len(accountRes.Rows) < 1 { + return nil, errors.Wrap(core.ErrNotFound, "could not find needed account state") + } + + return accountRes.Rows[0], nil +} + +func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core.AccountState) { + for _, tx := range b.Transactions { + getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + return s.getRecentAccountState(ctx, master.ID(), b.ID(), a, false) + } + + update := *tx.Account + + err := s.Parser.ParseAccountData(context.Background(), &update, getOtherAccountFunc) + if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + log.Error().Err(err).Str("addr", update.Address.Base64()).Msg("parse account data") + continue + } + + if reflect.DeepEqual(tx.Account, &update) { + continue + } + updates = append(updates, &update) + } + return updates +} + +func (s *Service) rescanAccountsWorker(b *core.Block) (updates []*core.AccountState) { + for _, shard := range b.Shards { + upd := s.rescanAccountsInBlock(b, shard) + updates = append(updates, upd...) + } + + upd := s.rescanAccountsInBlock(b, b) + updates = append(updates, upd...) + + return updates +} + +func (s *Service) rescanAccounts(masterBlocks []*core.Block) (lastScanned uint32) { + var ( + accountUpdates chan []*core.AccountState + scanWG sync.WaitGroup + ) + + scanWG.Add(len(masterBlocks)) + + for _, b := range masterBlocks { + go func(master *core.Block) { + defer scanWG.Done() + accountUpdates <- s.rescanAccountsWorker(master) + }(b) + + if b.SeqNo > lastScanned { + lastScanned = b.SeqNo + } + } + + go func() { + scanWG.Wait() + close(accountUpdates) + }() + + var allUpdates []*core.AccountState + for upd := range accountUpdates { + allUpdates = append(allUpdates, upd...) + } + + if err := s.AccountRepo.UpdateAccountStates(context.Background(), allUpdates); err != nil { + return 0 + } + + return lastScanned +} diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go new file mode 100644 index 00000000..4fbb72d7 --- /dev/null +++ b/internal/app/rescan/rescan.go @@ -0,0 +1,170 @@ +package rescan + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" +) + +var _ app.RescanService = (*Service)(nil) + +type Service struct { + *app.RescanConfig + + masterShard int64 + + run bool + mx sync.RWMutex + wg sync.WaitGroup +} + +func NewService(cfg *app.RescanConfig) *Service { + var s = new(Service) + + s.RescanConfig = cfg + + // validate config + if s.Workers < 1 { + s.Workers = 1 + } + + return s +} + +func (s *Service) running() bool { + s.mx.RLock() + defer s.mx.RUnlock() + + return s.run +} + +func (s *Service) Start() error { + s.mx.Lock() + s.run = true + s.mx.Unlock() + + s.wg.Add(1) + go s.rescanLoop() + + return nil +} + +func (s *Service) Stop() { + s.mx.Lock() + s.run = false + s.mx.Unlock() + + s.wg.Wait() +} + +func (s *Service) rescanLoop() { + defer s.wg.Done() + + lastMaster, err := s.BlockRepo.GetLastMasterBlock(context.Background()) + if err != nil { + log.Error().Err(err).Msg("cannot get last masterchain block") + return + } + toBlock := lastMaster.SeqNo + s.masterShard = lastMaster.Shard + + for s.running() { + tx, task, err := s.ContractRepo.GetUnfinishedRescanTask(context.Background()) + if err != nil { + if !errors.Is(err, core.ErrNotFound) { + log.Error().Err(err).Msg("get rescan task") + } + time.Sleep(time.Second) + continue + } + + if err := s.rescanRunTask(task, toBlock); err != nil { + _ = tx.Rollback() + log.Error().Err(err). + Int("id", task.ID). + Msg("run rescan task") + time.Sleep(time.Second) + continue + } + + if err := s.ContractRepo.SetRescanTask(context.Background(), tx, task); err != nil { + log.Error().Err(err).Msg("update rescan task") + time.Sleep(time.Second) + continue + } + } +} + +func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { + if task.AccountsRescanDone || task.AccountsLastMaster >= toBlock { + task.AccountsRescanDone = true + } else { + if task.AccountsLastMaster == 0 { + task.AccountsLastMaster = task.StartFrom - 1 + } + blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock) + if err != nil { + return errors.Wrap(err, "filter blocks for account states rescan") + } + if lastScanned := s.rescanAccounts(blocks); lastScanned != 0 { + task.AccountsLastMaster = lastScanned + } + } + + if task.MessagesRescanDone || task.MessagesLastMaster >= toBlock { + task.MessagesRescanDone = true + } else if task.AccountsRescanDone { + if task.MessagesLastMaster == 0 { + task.MessagesLastMaster = task.StartFrom - 1 + } + blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock) + if err != nil { + return errors.Wrap(err, "filter blocks for messages states rescan") + } + if lastScanned := s.rescanMessages(blocks); lastScanned != 0 { + task.MessagesLastMaster = lastScanned + } + } + + if task.AccountsRescanDone && task.MessagesRescanDone { + task.Finished = true + } + + return nil +} + +func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32) ([]*core.Block, error) { + workers := s.Workers + if delta := int(toBlock-fromBlock) + 1; delta < workers { + workers = delta + } + + req := &filter.BlocksReq{ + Workchain: new(int32), + Shard: new(int64), + WithShards: true, + WithTransactionAccountState: true, + WithTransactions: true, + WithTransactionMessages: false, + AfterSeqNo: new(uint32), + Order: "ASC", + Limit: workers, + } + *req.Workchain = -1 + *req.Shard = s.masterShard + *req.AfterSeqNo = fromBlock - 1 + + res, err := s.BlockRepo.FilterBlocks(context.Background(), req) + if err != nil { + return nil, err + } + + return res.Rows, nil +} diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go new file mode 100644 index 00000000..46424da6 --- /dev/null +++ b/internal/app/rescan/tx.go @@ -0,0 +1,135 @@ +package rescan + +import ( + "context" + "reflect" + "sync" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/internal/core" +) + +func (s *Service) rescanMessage(ctx context.Context, master core.BlockID, msg *core.Message) *core.Message { + // we must get account state's interfaces to properly determine message operation + // and to parse message accordingly + + // so for the source of the message we take the account state of the sender, + // which was updated just before the message was sent + + // for the destination of the given message, we take the account state of receiver, + // which was update just after the message was received + + if msg.SrcState == nil { + msg.SrcState, _ = s.getRecentAccountState(ctx, + master, + core.BlockID{ + Workchain: msg.SrcWorkchain, + Shard: msg.SrcShard, + SeqNo: msg.SrcBlockSeqNo, + }, + msg.SrcAddress, + false) + } + if msg.DstState == nil { + msg.DstState, _ = s.getRecentAccountState(ctx, + master, + core.BlockID{ + Workchain: msg.DstWorkchain, + Shard: msg.DstShard, + SeqNo: msg.DstBlockSeqNo, + }, + msg.DstAddress, + true) + } + + update := *msg + + err := s.Parser.ParseMessagePayload(ctx, &update) + if err != nil { + if !errors.Is(err, app.ErrImpossibleParsing) { + log.Error().Err(err). + Hex("msg_hash", msg.Hash). + Hex("src_tx_hash", msg.SrcTxHash). + Str("src_addr", msg.SrcAddress.String()). + Hex("dst_tx_hash", msg.DstTxHash). + Str("dst_addr", msg.DstAddress.String()). + Uint32("op_id", msg.OperationID). + Msg("parse message payload") + } + return nil + } + + if reflect.DeepEqual(msg, &update) { + return nil + } + + return &update +} + +func (s *Service) rescanMessagesInBlock(ctx context.Context, master, b *core.Block) (updates []*core.Message) { + for _, tx := range b.Transactions { + tx.InMsg.DstState = tx.Account + if got := s.rescanMessage(ctx, master.ID(), tx.InMsg); got != nil { + updates = append(updates, got) + } + + for _, out := range tx.OutMsg { + out.SrcState = tx.Account + if got := s.rescanMessage(ctx, master.ID(), out); got != nil { + updates = append(updates, got) + } + } + } + return updates +} + +func (s *Service) rescanMessagesWorker(m *core.Block) (updates []*core.Message) { + for _, shard := range m.Shards { + upd := s.rescanMessagesInBlock(context.Background(), m, shard) + updates = append(updates, upd...) + } + + upd := s.rescanMessagesInBlock(context.Background(), m, m) + updates = append(updates, upd...) + + return updates +} + +func (s *Service) rescanMessages(masterBlocks []*core.Block) (lastScanned uint32) { + var ( + msgUpdates chan []*core.Message + scanWG sync.WaitGroup + ) + + scanWG.Add(len(masterBlocks)) + + for _, b := range masterBlocks { + go func(master *core.Block) { + defer scanWG.Done() + msgUpdates <- s.rescanMessagesWorker(master) + }(b) + + if b.SeqNo > lastScanned { + lastScanned = b.SeqNo + } + } + + go func() { + scanWG.Wait() + close(msgUpdates) + }() + + var allUpdates []*core.Message + for upd := range msgUpdates { + allUpdates = append(allUpdates, upd...) + } + + if err := s.MessageRepo.UpdateMessages(context.Background(), allUpdates); err != nil { + return 0 + } + + return lastScanned +} From 64d8a5aa2f5a01f1feabaf5291681a9f388300a4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 21:31:46 +0700 Subject: [PATCH 059/186] [cmd] add rescan service --- cmd/rescan/rescan.go | 107 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 cmd/rescan/rescan.go diff --git a/cmd/rescan/rescan.go b/cmd/rescan/rescan.go new file mode 100644 index 00000000..67521f72 --- /dev/null +++ b/cmd/rescan/rescan.go @@ -0,0 +1,107 @@ +package rescan + +import ( + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/allisson/go-env" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/ton" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/internal/app" + "github.com/tonindexer/anton/internal/app/parser" + "github.com/tonindexer/anton/internal/app/rescan" + "github.com/tonindexer/anton/internal/core/repository" + "github.com/tonindexer/anton/internal/core/repository/account" + "github.com/tonindexer/anton/internal/core/repository/block" + "github.com/tonindexer/anton/internal/core/repository/contract" + "github.com/tonindexer/anton/internal/core/repository/msg" +) + +var Command = &cli.Command{ + Name: "rescan", + + Usage: "Updates account states and messages data", + + Action: func(ctx *cli.Context) error { + chURL := env.GetString("DB_CH_URL", "") + pgURL := env.GetString("DB_PG_URL", "") + + conn, err := repository.ConnectDB(ctx.Context, chURL, pgURL) + if err != nil { + return errors.Wrap(err, "cannot connect to a database") + } + + contractRepo := contract.NewRepository(conn.PG) + + interfaces, err := contractRepo.GetInterfaces(ctx.Context) + if err != nil { + return errors.Wrap(err, "get interfaces") + } + if len(interfaces) == 0 { + return errors.New("no contract interfaces") + } + + def, err := contractRepo.GetDefinitions(ctx.Context) + if err != nil { + return errors.Wrap(err, "get definitions") + } + err = abi.RegisterDefinitions(def) + if err != nil { + return errors.Wrap(err, "get definitions") + } + + client := liteclient.NewConnectionPool() + api := ton.NewAPIClient(client, ton.ProofCheckPolicyUnsafe).WithRetry() + for _, addr := range strings.Split(env.GetString("LITESERVERS", ""), ",") { + split := strings.Split(addr, "|") + if len(split) != 2 { + return fmt.Errorf("wrong server address format '%s'", addr) + } + host, key := split[0], split[1] + if err := client.AddConnection(ctx.Context, host, key); err != nil { + return errors.Wrapf(err, "cannot add connection with %s host and %s key", host, key) + } + } + bcConfig, err := app.GetBlockchainConfig(ctx.Context, api) + if err != nil { + return errors.Wrap(err, "cannot get blockchain config") + } + + p := parser.NewService(&app.ParserConfig{ + BlockchainConfig: bcConfig, + ContractRepo: contractRepo, + }) + i := rescan.NewService(&app.RescanConfig{ + ContractRepo: contractRepo, + AccountRepo: account.NewRepository(conn.CH, conn.PG), + BlockRepo: block.NewRepository(conn.CH, conn.PG), + MessageRepo: msg.NewRepository(conn.CH, conn.PG), + Parser: p, + Workers: env.GetInt("RESCAN_WORKERS", 4), + }) + if err = i.Start(); err != nil { + return err + } + + c := make(chan os.Signal, 1) + done := make(chan struct{}, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-c + i.Stop() + conn.Close() + done <- struct{}{} + }() + + <-done + + return nil + }, +} From 2ff56afc1d912fa2a4980d6d5e8e821f23ebfba0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 21:32:10 +0700 Subject: [PATCH 060/186] [cmd] contract: add subcommand for rescan --- cmd/contract/interface.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index d1ff9a37..7f30d78e 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -207,6 +207,43 @@ var Command = &cli.Command{ } } + return nil + }, + }, + { + Name: "rescan", + Usage: "Updates account states and messages data from the given block", + + ArgsUsage: "[from_block]", + + Action: func(ctx *cli.Context) error { + pg := bun.NewDB( + sql.OpenDB( + pgdriver.NewConnector( + pgdriver.WithDSN(env.GetString("DB_PG_URL", "")), + ), + ), + pgdialect.New(), + ) + if err := pg.Ping(); err != nil { + return errors.Wrapf(err, "cannot ping postgresql") + } + + contractRepo := contract.NewRepository(pg) + + fromBlock, err := strconv.ParseUint(ctx.Args().First(), 10, 32) + if err != nil { + return errors.Wrap(err, "wrong from_block argument") + } + + err = contractRepo.CreateNewRescanTask(ctx.Context, uint32(fromBlock)) + if err != nil { + if errors.Is(err, core.ErrAlreadyExists) { + return errors.New("there is already one unfinished task") + } + return errors.Wrapf(err, "create new rescan task") + } + return nil }, }, From 592b22ac8a7bbbec644830812bcafbce433e33a6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 21:33:14 +0700 Subject: [PATCH 061/186] docker-compose.yml: add rescan service --- docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index c3bdaf53..64507a7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,18 @@ services: WORKERS: ${WORKERS} LITESERVERS: ${LITESERVERS} DEBUG_LOGS: ${DEBUG_LOGS} + rescan: + <<: *anton-service + depends_on: + <<: *anton-deps + migrations: + condition: service_completed_successfully + command: rescan + environment: + <<: *anton-env + RESCAN_WORKERS: ${RESCAN_WORKERS} + LITESERVERS: ${LITESERVERS} + DEBUG_LOGS: ${DEBUG_LOGS} web: <<: *anton-service depends_on: From f0e61637b5c7f3fe5b7fd28d827cf4cbb42059d4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 21:33:47 +0700 Subject: [PATCH 062/186] .env.example: add RESCAN_WORKERS --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index f1ad724d..5b9641f4 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ FROM_BLOCK=25000000 LITESERVERS=135.181.177.59:53312|aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= DEBUG_LOGS=false WORKERS=4 +RESCAN_WORKERS=4 # LITESERVERS=65.108.141.177:17439|0MIADpLH4VQn+INHfm0FxGiuZZAA8JfTujRqQugkkA8= # testnet \ No newline at end of file From 7063cdc38dfad92643ac906326fa35e915cc0a51 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 22:47:26 +0700 Subject: [PATCH 063/186] [core] rescan_task table: fix bun tag on start_from field --- internal/core/contract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/contract.go b/internal/core/contract.go index 9f6316af..84db4ccc 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -43,7 +43,7 @@ type RescanTask struct { ID int `bun:",pk,autoincrement"` Finished bool `bun:"finished,notnull"` - StartFrom uint32 `bun:"start_from_start_from_masterchain_seq_nomasterchain_seq_no,notnull"` + StartFrom uint32 `bun:"start_from_masterchain_seq_no,notnull"` AccountsLastMaster uint32 `bun:"accounts_last_masterchain_seq_no,notnull"` AccountsRescanDone bool `bun:",notnull"` MessagesLastMaster uint32 `bun:"messages_last_masterchain_seq_no,notnull"` From ddd60165727a02c210e4c7ed4d74a3660d07fa9c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:22:23 +0700 Subject: [PATCH 064/186] [app] rescan: add buffer to worker channels --- internal/app/rescan/account.go | 6 +++++- internal/app/rescan/tx.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index c92bc421..e1793fe0 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -54,6 +54,10 @@ func (s *Service) getRecentAccountState(ctx context.Context, master, b core.Bloc func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core.AccountState) { for _, tx := range b.Transactions { + if tx.Account == nil { + continue + } + getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { return s.getRecentAccountState(ctx, master.ID(), b.ID(), a, false) } @@ -88,7 +92,7 @@ func (s *Service) rescanAccountsWorker(b *core.Block) (updates []*core.AccountSt func (s *Service) rescanAccounts(masterBlocks []*core.Block) (lastScanned uint32) { var ( - accountUpdates chan []*core.AccountState + accountUpdates = make(chan []*core.AccountState, len(masterBlocks)) scanWG sync.WaitGroup ) diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 46424da6..a0e72d6d 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -100,7 +100,7 @@ func (s *Service) rescanMessagesWorker(m *core.Block) (updates []*core.Message) func (s *Service) rescanMessages(masterBlocks []*core.Block) (lastScanned uint32) { var ( - msgUpdates chan []*core.Message + msgUpdates = make(chan []*core.Message, len(masterBlocks)) scanWG sync.WaitGroup ) From 0c39eedadcfb58e6bda0f80290466893ba60d895 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:22:44 +0700 Subject: [PATCH 065/186] docker-compose.dev.yml: add rescan service rewrites --- docker-compose.dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2d9bd4f5..5ca79a03 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,6 +7,8 @@ x-anton-rewrites: &anton-rewrites services: indexer: <<: *anton-rewrites + rescan: + <<: *anton-rewrites web: <<: *anton-rewrites migrations: From 0c969144795a00cb8fd2affb5ccfc42c5d2b86b6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:27:01 +0700 Subject: [PATCH 066/186] [parser] callPossibleGetMethods: print address on getting minter account state error --- internal/app/parser/get.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index a4aae5ea..6425a387 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -271,7 +271,7 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo } collection, err := others(ctx, *acc.MinterAddress) if err != nil { - log.Error().Err(err).Msg("get nft collection state") + log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get nft collection state") return } s.getNFTItemContent(ctx, collection, exec.Returns[1].(*big.Int), exec.Returns[4].(*cell.Cell), acc) //nolint:forcetypeassert // panic on wrong interface @@ -290,7 +290,7 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo } minter, err := others(ctx, *acc.MinterAddress) if err != nil { - log.Error().Err(err).Msg("get jetton minter state") + log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get jetton minter state") return } s.checkJettonMinter(ctx, minter, acc.OwnerAddress, acc) From 108b9f2db593a0c02b41474c452c20670164da5e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:30:29 +0700 Subject: [PATCH 067/186] [abi] vmParseCell: get type for format from typeNameMap --- abi/get_emulator.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index e2851b5f..954acfc1 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "math/big" + "reflect" "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/txemulator" @@ -334,7 +335,17 @@ func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { default: d, ok := registeredDefinitions[desc.Format] if !ok { - return nil, fmt.Errorf("cannot find definition for '%s' format", desc.Format) + t, ok := typeNameMap[desc.Format] + if !ok { + return nil, fmt.Errorf("cannot find definition or type for '%s' format", desc.Format) + } + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + tv := reflect.New(t).Interface() + if err := tutlb.LoadFromCell(tv, c.BeginParse()); err != nil { + return nil, fmt.Errorf("load type '%s' from cell: %w", desc.Format, err) + } } parsed, err := d.FromCell(c) if err != nil { From dd9c88b39de938f9165934ba53555c32085c4ad1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:30:40 +0700 Subject: [PATCH 068/186] main.go: add rescan command --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 44ae9093..98960277 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/tonindexer/anton/cmd/db" "github.com/tonindexer/anton/cmd/indexer" "github.com/tonindexer/anton/cmd/label" + "github.com/tonindexer/anton/cmd/rescan" "github.com/tonindexer/anton/cmd/web" ) @@ -40,6 +41,7 @@ func main() { archive.Command, contract.Command, label.Command, + rescan.Command, }, } if err := app.Run(os.Args); err != nil { From 7c72044b0f0288b5526b7246ca541080bf081a4e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 14 Feb 2024 23:30:59 +0700 Subject: [PATCH 069/186] [app] rescan: add log on start --- internal/app/rescan/rescan.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 4fbb72d7..a29014fc 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -53,6 +53,10 @@ func (s *Service) Start() error { s.wg.Add(1) go s.rescanLoop() + log.Info(). + Int("workers", s.Workers). + Msg("rescan started") + return nil } From 0e412488c323757fa70fb1305f3aac1163c64737 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 15:03:16 +0700 Subject: [PATCH 070/186] [app] rescan: skip wallet accounts --- abi/known/known.go | 50 ++++++++++++++++++++++++---------- internal/app/rescan/account.go | 6 ++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/abi/known/known.go b/abi/known/known.go index 98918cdd..44b25a48 100644 --- a/abi/known/known.go +++ b/abi/known/known.go @@ -12,21 +12,41 @@ var ( JettonWallet abi.ContractName = "jetton_wallet" ) +var ( + walletInterfacesSet = map[abi.ContractName]struct{}{ + "wallet_v1r1": {}, + "wallet_v1r2": {}, + "wallet_v1r3": {}, + "wallet_v2r1": {}, + "wallet_v2r2": {}, + "wallet_v3r1": {}, + "wallet_v3r2": {}, + "wallet_v4r1": {}, + "wallet_v4r2": {}, + "wallet_lockup": {}, + "wallet_highload_v1r1": {}, + "wallet_highload_v1r2": {}, + "wallet_highload_v2r1": {}, + "wallet_highload_v2r2": {}, + } + walletInterfacesList []abi.ContractName +) + +func init() { + for w := range walletInterfacesSet { + walletInterfacesList = append(walletInterfacesList, w) + } +} + func GetAllWalletNames() []abi.ContractName { - return []abi.ContractName{ - "wallet_v1r1", - "wallet_v1r2", - "wallet_v1r3", - "wallet_v2r1", - "wallet_v2r2", - "wallet_v3r1", - "wallet_v3r2", - "wallet_v4r1", - "wallet_v4r2", - "wallet_lockup", - "wallet_highload_v1r1", - "wallet_highload_v1r2", - "wallet_highload_v2r1", - "wallet_highload_v2r2", + return walletInterfacesList +} + +func IsOnlyWalletInterfaces(interfaces []abi.ContractName) bool { + for _, i := range interfaces { + if _, ok := walletInterfacesSet[i]; !ok { + return false + } } + return true } diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index e1793fe0..2085328d 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/tonindexer/anton/abi/known" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" @@ -57,6 +58,11 @@ func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core. if tx.Account == nil { continue } + if known.IsOnlyWalletInterfaces(tx.Account.Types) { + // we do not want to emulate wallet get-methods once again, + // as there are lots of them, so it takes a lot of CPU usage + continue + } getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { return s.getRecentAccountState(ctx, master.ID(), b.ID(), a, false) From ced6f467f8a81dc18023814e0d50fde49f27969f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 15:14:04 +0700 Subject: [PATCH 071/186] README.md: add description for rescanning --- README.md | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 79e47441..4b230923 100644 --- a/README.md +++ b/README.md @@ -103,17 +103,18 @@ cp .env.example .env nano .env ``` -| Name | Description | Default | Example | -|----------------------|-----------------------------------|---------|--------------------------------------------------------------------| -| `DB_NAME` | Database name | | idx | -| `DB_USERNAME` | Database username | | user | -| `DB_PASSWORD` | Database password | | pass | -| `DB_CH_URL` | Clickhouse URL to connect to | | clickhouse://clickhouse:9000/db_name?sslmode=disable | -| `DB_PG_URL` | PostgreSQL URL to connect to | | postgres://username:password@postgres:5432/db_name?sslmode=disable | -| `FROM_BLOCK` | Master chain seq_no to start from | 1 | 23532000 | -| `WORKERS` | Number of indexer workers | 4 | 8 | -| `LITESERVERS` | Lite servers to connect to | | 135.181.177.59:53312 aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= | -| `DEBUG_LOGS` | Debug logs enabled | false | true | +| Name | Description | Default | Example | +|------------------|-----------------------------------|---------|--------------------------------------------------------------------| +| `DB_NAME` | Database name | | idx | +| `DB_USERNAME` | Database username | | user | +| `DB_PASSWORD` | Database password | | pass | +| `DB_CH_URL` | Clickhouse URL to connect to | | clickhouse://clickhouse:9000/db_name?sslmode=disable | +| `DB_PG_URL` | PostgreSQL URL to connect to | | postgres://username:password@postgres:5432/db_name?sslmode=disable | +| `FROM_BLOCK` | Master chain seq_no to start from | 1 | 23532000 | +| `WORKERS` | Number of indexer workers | 4 | 8 | +| `RESCAN_WORKERS` | Number of rescan workers | 4 | 8 | +| `LITESERVERS` | Lite servers to connect to | | 135.181.177.59:53312 aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= | +| `DEBUG_LOGS` | Debug logs enabled | false | true | ### Building @@ -160,7 +161,7 @@ You can add them through this command: docker compose exec web sh -c "anton contract /var/anton/known/*.json" ``` -### Schema migration +### Database schema migration ```shell # run migrations service on running compose @@ -209,13 +210,13 @@ docker compose \ ## Using -### Show archive nodes from global config +### Showing archive nodes from global config ```shell docker run tonindexer/anton archive [--testnet] ``` -### Insert contract interface +### Inserting contract interface ```shell # add from stdin @@ -224,13 +225,13 @@ cat abi/known/tep81_dns.json | docker compose exec -T web anton contract --stdin docker compose exec web anton contract "/var/anton/known/tep81_dns.json" ``` -### Delete contract interface +### Deleting contract interface ```shell docker compose exec web anton contract delete "dns_nft_item" ``` -### Add address label +### Addding address label ```shell docker compose exec web anton label "EQDj5AA8mQvM5wJEQsFFFof79y3ZsuX6wowktWQFhz_Anton" "anton.tools" @@ -238,3 +239,17 @@ docker compose exec web anton label "EQDj5AA8mQvM5wJEQsFFFof79y3ZsuX6wowktWQFhz_ # known tonscan labels docker compose exec web anton label --tonscan ``` + +## Rescanning + +If you've modified a contract interface by adding new ones or deleting old ones, +you can reparse account states and messages. + +To accomplish this, create a task specifying the masterchain block number from which to start rescanning. +The rescan service will then iterate through the database data and update rows based on the new contract descriptions. + +### Adding a rescan task + +```shell +docker compose exec web anton contract rescan 24400000 +``` From 604cd61278e5f5b2641209df55730bf059f94bf5 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 15:44:37 +0700 Subject: [PATCH 072/186] [abi] vmParseCell: fix custom type parsing --- abi/get_emulator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 954acfc1..965b7c10 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -346,6 +346,7 @@ func vmParseCell(c *cell.Cell, desc *VmValueDesc) (any, error) { if err := tutlb.LoadFromCell(tv, c.BeginParse()); err != nil { return nil, fmt.Errorf("load type '%s' from cell: %w", desc.Format, err) } + return tv, nil } parsed, err := d.FromCell(c) if err != nil { From a3fc765e4cb76db56e018e6772ac767ab2435d3a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 15:46:25 +0700 Subject: [PATCH 073/186] [rescan] rescanMessagesInBlock: check that incoming message is not nil --- internal/app/rescan/tx.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index a0e72d6d..f988238a 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -71,9 +71,11 @@ func (s *Service) rescanMessage(ctx context.Context, master core.BlockID, msg *c func (s *Service) rescanMessagesInBlock(ctx context.Context, master, b *core.Block) (updates []*core.Message) { for _, tx := range b.Transactions { - tx.InMsg.DstState = tx.Account - if got := s.rescanMessage(ctx, master.ID(), tx.InMsg); got != nil { - updates = append(updates, got) + if tx.InMsg != nil { + tx.InMsg.DstState = tx.Account + if got := s.rescanMessage(ctx, master.ID(), tx.InMsg); got != nil { + updates = append(updates, got) + } } for _, out := range tx.OutMsg { From f0b11f5da236c1e50698dc118abef6d7836b4773 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 15:46:41 +0700 Subject: [PATCH 074/186] [rescan] filterBlocksForRescan: join messages --- internal/app/rescan/rescan.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index a29014fc..26be0d06 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -113,7 +113,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { if task.AccountsLastMaster == 0 { task.AccountsLastMaster = task.StartFrom - 1 } - blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock) + blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock, false) if err != nil { return errors.Wrap(err, "filter blocks for account states rescan") } @@ -128,7 +128,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { if task.MessagesLastMaster == 0 { task.MessagesLastMaster = task.StartFrom - 1 } - blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock) + blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock, true) if err != nil { return errors.Wrap(err, "filter blocks for messages states rescan") } @@ -144,7 +144,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { return nil } -func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32) ([]*core.Block, error) { +func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withMessages bool) ([]*core.Block, error) { workers := s.Workers if delta := int(toBlock-fromBlock) + 1; delta < workers { workers = delta @@ -156,7 +156,7 @@ func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32) ([]*core.Bloc WithShards: true, WithTransactionAccountState: true, WithTransactions: true, - WithTransactionMessages: false, + WithTransactionMessages: withMessages, AfterSeqNo: new(uint32), Order: "ASC", Limit: workers, From 75f690af445ffd8a6e42ac6f4312cf5e633df439 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 16:28:19 +0700 Subject: [PATCH 075/186] [rescan] getRecentAccountState: fix ordering --- internal/app/rescan/account.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 2085328d..5417b6ac 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -33,13 +33,14 @@ func (s *Service) getRecentAccountState(ctx context.Context, master, b core.Bloc Addresses: []*addr.Address{&a}, Workchain: &boundBlock.Workchain, Shard: &boundBlock.Shard, - Order: "DESC", Limit: 1, } if afterBlock { accountReq.BlockSeqNoBeq = &boundBlock.SeqNo + accountReq.Order = "ASC" } else { accountReq.BlockSeqNoLeq = &boundBlock.SeqNo + accountReq.Order = "DESC" } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) From 239cd667744234a9f28d2421f200211ac9971bc8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 16:29:18 +0700 Subject: [PATCH 076/186] [filter] account repo: order by block_seq_no, if filtered by that column --- internal/core/repository/account/filter.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index 700598bf..c8065e3b 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -113,6 +113,9 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts if f.BlockSeqNoLeq != nil { q = q.Where(prefix+"block_seq_no <= ?", *f.BlockSeqNoLeq) } + if f.BlockSeqNoBeq != nil { + q = q.Where(prefix+"block_seq_no >= ?", *f.BlockSeqNoBeq) + } if len(f.ContractTypes) > 0 { q = q.Where(prefix+"types && ?", pgdialect.Array(f.ContractTypes)) @@ -132,7 +135,11 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts } } if f.Order != "" { - q = q.Order(statesTable + "last_tx_lt " + strings.ToUpper(f.Order)) + orderBy := "last_tx_lt" + if f.BlockSeqNoLeq != nil || f.BlockSeqNoBeq != nil { + orderBy = "block_seq_no" + } + q = q.Order(statesTable + orderBy + " " + strings.ToUpper(f.Order)) } if total < 100000 && f.LatestState { From 54fa825b5d1ffacf0b69121b5939f90924110134 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 16:30:06 +0700 Subject: [PATCH 077/186] [abi] add dedust contracts --- abi/known/dedust_v2.json | 866 +++++++++++++++++++++++++++++++++++++++ abi/tlb_types.go | 77 ++++ 2 files changed, 943 insertions(+) create mode 100644 abi/known/dedust_v2.json diff --git a/abi/known/dedust_v2.json b/abi/known/dedust_v2.json new file mode 100644 index 00000000..a985e899 --- /dev/null +++ b/abi/known/dedust_v2.json @@ -0,0 +1,866 @@ +[ + { + "interface_name": "dedust_v2_factory", + "addresses": [ + "EQBfBWT7X2BHg9tXAxzhz2aKiNTU1tpt5NsiK0uSDW_YAJ67" + ], + "definitions": { + "pool_params": [ + { + "name": "is_stable", + "tlb_type": "bool" + }, + { + "name": "asset0", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset1", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + "in_messages": [ + { + "op_name": "dedust_v2_transfer_ownership", + "op_code": "0xca61554e", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "new_owner_addr", + "tlb_type": "addr", + "format": "addr" + } + ] + }, + { + "op_name": "dedust_v2_accept_ownership", + "op_code": "0xdee60404", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + } + ] + }, + { + "op_name": "dedust_v2_cancel_ownership_transfer", + "op_code": "0x16cb7fc2", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + } + ] + }, + { + "op_name": "dedust_v2_install_vault_code", + "op_code": "0xbc3f26f6", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset_type", + "tlb_type": "## 4", + "format": "uint8" + }, + { + "name": "code_version", + "tlb_type": "## 16", + "format": "uint16" + }, + { + "name": "code", + "tlb_type": "^", + "format": "cell" + } + ] + }, + { + "op_name": "dedust_v2_create_vault", + "op_code": "0x21cfe02b", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + { + "op_name": "dedust_v2_create_legacy_jetton_vault", + "op_code": "0xc9a5752d", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "minter_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "resolver_addr", + "tlb_type": "addr", + "format": "addr" + } + ] + }, + { + "op_name": "dedust_v2_upgrade_vault", + "op_code": "0x25d66911", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + { + "op_name": "dedust_v2_destroy_non_ready_vault", + "op_code": "0x8a518d0d", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + { + "op_name": "dedust_v2_upgrade", + "op_code": "0xdf4a27aa", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "code_version", + "tlb_type": "## 16", + "format": "uint16" + }, + { + "name": "code", + "tlb_type": "^", + "format": "cell" + } + ] + }, + { + "op_name": "dedust_v2_reset_gas", + "op_code": "0x9f3f0937", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + } + ] + }, + { + "op_name": "dedust_v2_install_pool_code", + "op_code": "0xa3e45df1", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "code_version", + "tlb_type": "## 16", + "format": "uint16" + }, + { + "name": "code", + "tlb_type": "^", + "format": "cell" + } + ] + }, + { + "op_name": "dedust_v2_create_volatile_pool", + "op_code": "0x97d51f2f", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset0", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset1", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + { + "op_name": "dedust_v2_create_stable_pool", + "op_code": "0x7c40ac87", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "asset0", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset0_decimals", + "tlb_type": "## 8", + "format": "uint8" + }, + { + "name": "asset1", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset1_decimals", + "tlb_type": "## 8", + "format": "uint8" + } + ] + }, + { + "op_name": "dedust_v2_upgrade_pool", + "op_code": "0x53e252ae", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "is_stable", + "tlb_type": "bool" + }, + { + "name": "asset0", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset1", + "tlb_type": ".", + "format": "dedustAsset" + } + ] + }, + { + "op_name": "dedust_v2_configure_pool_trade_fee", + "op_code": "0xf99d79f3", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "pool_params", + "tlb_type": ".", + "format": "pool_params" + }, + { + "name": "trade_fee", + "tlb_type": "## 16", + "format": "uint16" + } + ] + }, + { + "op_name": "dedust_v2_install_liquidity_deposit_code", + "op_code": "0x99a84311", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "code_version", + "tlb_type": "## 16", + "format": "uint16" + }, + { + "name": "code", + "tlb_type": "^", + "format": "cell" + } + ] + }, + { + "op_name": "dedust_v2_create_liquidity_deposit", + "op_code": "0xf04ec526", + "body": [ + { + "name": "query_id", + "tlb_type": "## 64", + "format": "uint64" + }, + { + "name": "proof", + "tlb_type": "^", + "format": "cell" + }, + { + "name": "owner_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "pool_params", + "tlb_type": ".", + "format": "pool_params" + }, + { + "name": "next", + "tlb_type": "^", + "format": "struct", + "struct_fields": [ + { + "name": "asset0_target_balance", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "asset1_target_balance", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "deposit_asset", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "deposit_amount", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "min_lp_amount", + "tlb_type": ".", + "format": "coins" + } + ] + }, + { + "name": "fulfill_payload", + "tlb_type": "maybe ^", + "format": "cell", + "optional": true + }, + { + "name": "reject_payload", + "tlb_type": "maybe ^", + "format": "cell", + "optional": true + } + ] + } + ], + "get_methods": [ + { + "name": "get_ownership", + "return_values": [ + { + "name": "owner_addr", + "stack_type": "slice", + "format": "addr" + }, + { + "name": "pending_owner_addr", + "stack_type": "slice", + "format": "addr" + }, + { + "name": "can_accept_after", + "stack_type": "int", + "format": "uint32" + } + ] + }, + { + "name": "get_version", + "return_values": [ + { + "name": "version", + "stack_type": "int", + "format": "uint16" + } + ] + }, + { + "name": "get_vault_address", + "arguments": [ + { + "name": "asset", + "stack_type": "slice", + "format": "dedustAsset" + } + ], + "return_values": [ + { + "name": "vault_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_pool_address", + "arguments": [ + { + "name": "is_stable", + "stack_type": "int", + "format": "bool" + }, + { + "name": "asset0", + "stack_type": "slice", + "format": "dedustAsset" + }, + { + "name": "asset1", + "stack_type": "slice", + "format": "dedustAsset" + } + ], + "return_values": [ + { + "name": "pool_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_liquidity_deposit_address", + "arguments": [ + { + "name": "owner_addr", + "stack_type": "slice", + "format": "addr" + }, + { + "name": "is_stable", + "stack_type": "int", + "format": "bool" + }, + { + "name": "asset0", + "stack_type": "slice", + "format": "dedustAsset" + }, + { + "name": "asset1", + "stack_type": "slice", + "format": "dedustAsset" + } + ], + "return_values": [ + { + "name": "liquidity_deposit_addr", + "stack_type": "slice", + "format": "addr" + } + ] + } + ] + }, + { + "interface_name": "dedust_v2_pool", + "get_methods": [ + { + "name": "get_version", + "return_values": [ + { + "name": "version", + "stack_type": "int", + "format": "uint16" + } + ] + }, + { + "name": "is_stable", + "return_values": [ + { + "name": "version", + "stack_type": "int", + "format": "bool" + } + ] + }, + { + "name": "get_trade_fee", + "return_values": [ + { + "name": "numerator", + "stack_type": "int", + "format": "uint16" + }, + { + "name": "denominator", + "stack_type": "int", + "format": "uint16" + } + ] + }, + { + "name": "get_reserves", + "return_values": [ + { + "name": "asset0_reserve", + "stack_type": "int", + "format": "bigInt" + }, + { + "name": "asset1_reserve", + "stack_type": "int", + "format": "bigInt" + } + ] + }, + { + "name": "get_protocol_fees", + "return_values": [ + { + "name": "asset0_protocol_fee", + "stack_type": "int", + "format": "bigInt" + }, + { + "name": "asset1_protocol_fee", + "stack_type": "int", + "format": "bigInt" + } + ] + }, + { + "name": "get_assets", + "return_values": [ + { + "name": "asset0", + "stack_type": "slice", + "format": "dedustAsset" + }, + { + "name": "asset1", + "stack_type": "slice", + "format": "dedustAsset" + } + ] + } + ], + "out_messages": [ + { + "op_name": "dedust_v2_deposit", + "op_code": "0xb544f4a4", + "type": "external_out", + "body": [ + { + "name": "sender_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "amount0", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "amount1", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "reserve0", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "reserve1", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "liquidity", + "tlb_type": ".", + "format": "coins" + } + ] + }, + { + "op_name": "dedust_v2_withdrawal", + "op_code": "0x3aa870a6", + "type": "external_out", + "body": [ + { + "name": "sender_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "liquidity", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "amount0", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "amount1", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "reserve0", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "reserve1", + "tlb_type": ".", + "format": "coins" + } + ] + }, + { + "op_name": "dedust_v2_swap", + "op_code": "0x9c610de3", + "type": "external_out", + "body": [ + { + "name": "asset_in", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "asset_out", + "tlb_type": ".", + "format": "dedustAsset" + }, + { + "name": "amount_in", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "amount_out", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "next", + "tlb_type": "^", + "format": "struct", + "struct_fields": [ + { + "name": "sender_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "referral_addr", + "tlb_type": "addr", + "format": "addr" + }, + { + "name": "reserve0", + "tlb_type": ".", + "format": "coins" + }, + { + "name": "reserve1", + "tlb_type": ".", + "format": "coins" + } + ] + } + ] + } + ] + }, + { + "interface_name": "dedust_v2_liquidity_deposit", + "get_methods": [ + { + "name": "get_factory_addr", + "return_values": [ + { + "name": "factory_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_owner_addr", + "return_values": [ + { + "name": "owner_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_pool_addr", + "return_values": [ + { + "name": "pool_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_pool_params", + "return_values": [ + { + "name": "is_stable", + "stack_type": "int", + "format": "bool" + }, + { + "name": "asset0", + "stack_type": "slice", + "format": "dedustAsset" + }, + { + "name": "asset1", + "stack_type": "slice", + "format": "dedustAsset" + } + ] + }, + { + "name": "get_target_balances", + "return_values": [ + { + "name": "asset0_target_balance", + "stack_type": "int", + "format": "bigInt" + }, + { + "name": "asset1_target_balance", + "stack_type": "int", + "format": "bigInt" + } + ] + }, + { + "name": "get_balances", + "return_values": [ + { + "name": "asset0_balance", + "stack_type": "int", + "format": "bigInt" + }, + { + "name": "asset1_balance", + "stack_type": "int", + "format": "bigInt" + } + ] + }, + { + "name": "is_processing", + "return_values": [ + { + "name": "status", + "stack_type": "int", + "format": "bool" + } + ] + }, + { + "name": "get_min_lp_amount", + "return_values": [ + { + "name": "min_lp_amount", + "stack_type": "int", + "format": "bigInt" + } + ] + } + ] + }, + { + "interface_name": "dedust_v2_vault", + "get_methods": [ + { + "name": "get_version", + "return_values": [ + { + "name": "version", + "stack_type": "int", + "format": "uint16" + } + ] + }, + { + "name": "get_factory_addr", + "return_values": [ + { + "name": "factory_addr", + "stack_type": "slice", + "format": "addr" + } + ] + }, + { + "name": "get_asset", + "return_values": [ + { + "name": "asset", + "stack_type": "slice", + "format": "dedustAsset" + } + ] + } + ] + } +] diff --git a/abi/tlb_types.go b/abi/tlb_types.go index 472c1e17..3ccd0f8e 100644 --- a/abi/tlb_types.go +++ b/abi/tlb_types.go @@ -1,6 +1,8 @@ package abi import ( + "encoding/json" + "fmt" "math/big" "reflect" @@ -59,6 +61,80 @@ func (x *StringSnake) LoadFromCell(loader *cell.Slice) error { return nil } +type DedustAssetNative struct { + _ tlb.Magic `tlb:"$0000"` +} + +type DedustAssetJetton struct { + _ tlb.Magic `tlb:"$0001"` + Workchain int8 `tlb:"## 8"` + Address []byte `tlb:"bits 256"` +} + +type DedustAssetExtraCurrency struct { + _ tlb.Magic `tlb:"$0010"` + CurrencyID int32 `tlb:"## 32"` +} + +type DedustAsset struct { + Asset any `tlb:"."` +} + +func (x *DedustAsset) LoadFromCell(loader *cell.Slice) error { + pfx, err := loader.LoadUInt(4) + if err != nil { + return err + } + + switch pfx { + case 0b0000: + x.Asset = (*DedustAssetNative)(nil) + return nil + case 0b0001: + x.Asset = new(DedustAssetJetton) + err = tlb.LoadFromCell(x.Asset, loader, true) + if err != nil { + return fmt.Errorf("failed to parse DedustAssetJetton: %w", err) + } + return nil + case 0b0010: + x.Asset = new(DedustAssetExtraCurrency) + err = tlb.LoadFromCell(x.Asset, loader, true) + if err != nil { + return fmt.Errorf("failed to parse DedustAssetExtraCurrency: %w", err) + } + return nil + } + + return fmt.Errorf("unknown dedust asset type: %x", pfx) +} + +func (x *DedustAsset) MarshalJSON() ([]byte, error) { + if x == nil || x.Asset == nil { + return json.Marshal(nil) + } + + var ret struct { + Type string `json:"type"` + Workchain *int8 `json:"workchain,omitempty"` + Address []byte `json:"address,omitempty"` + CurrencyID int32 `json:"currency_id,omitempty"` + } + switch v := x.Asset.(type) { + case *DedustAssetNative: + ret.Type = "native" + case *DedustAssetJetton: + ret.Type = "jetton" + ret.Workchain = &v.Workchain + ret.Address = v.Address + case *DedustAssetExtraCurrency: + ret.Type = "extra_currency" + ret.CurrencyID = v.CurrencyID + } + + return json.Marshal(ret) +} + var ( typeNameRMap = map[reflect.Type]TLBType{ reflect.TypeOf([]uint8{}): TLBBytes, @@ -82,6 +158,7 @@ var ( TLBAddr: reflect.TypeOf((*address.Address)(nil)), TLBString: reflect.TypeOf((*StringSnake)(nil)), "telemintText": reflect.TypeOf((*TelemintText)(nil)), + "dedustAsset": reflect.TypeOf((*DedustAsset)(nil)), } registeredDefinitions = map[TLBType]TLBFieldsDesc{} From da9cb45de7752474ec8bcf759acc7de3f1b0e441 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 23:12:33 +0700 Subject: [PATCH 078/186] [repo] account: return all account interfaces changes --- internal/core/account.go | 4 + internal/core/repository/account/account.go | 50 ++++++++ .../core/repository/account/account_test.go | 113 ++++++++++++++++++ internal/core/rndm/account.go | 6 + 4 files changed, 173 insertions(+) diff --git a/internal/core/account.go b/internal/core/account.go index 7188e30f..cea8578d 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -136,4 +136,8 @@ type AccountRepository interface { AddAccountStates(ctx context.Context, tx bun.Tx, states []*AccountState) error UpdateAccountStates(ctx context.Context, states []*AccountState) error + + // GetAllAccountInterfaces returns transaction LT, on which contract interface was updated. + // It also considers, that contract can be both upgraded and downgraded. + GetAllAccountInterfaces(context.Context, addr.Address) (map[uint64][]abi.ContractName, error) } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 064ea0dd..e9a9deef 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "reflect" "strings" "github.com/pkg/errors" @@ -11,6 +12,7 @@ import ( "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository" @@ -277,3 +279,51 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A return nil } + +func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address) (map[uint64][]abi.ContractName, error) { + var ret []struct { + ChangeTxLT uint64 + Types []abi.ContractName `bun:"type:text[],array"` + } + + minTxLtSubQ := r.pg.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("min(last_tx_lt)"). + Where("address = ?", &a) + + err := r.pg.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS change_tx_lt"). + ColumnExpr("types"). + Where("address = ? AND last_tx_lt = (?)", &a, minTxLtSubQ). + UnionAll( + r.pg.NewSelect(). + TableExpr("(?) AS diff", + r.pg.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS tx_lt"). + ColumnExpr("types"). + ColumnExpr("lead(last_tx_lt) OVER (ORDER BY last_tx_lt ASC) AS next_tx_lt"). + ColumnExpr("lead(types) OVER (ORDER BY last_tx_lt ASC) AS next_types"). + Where("address = ?", &a). + Order("tx_lt ASC")). + ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN tx_lt ELSE next_tx_lt END AS change_tx_lt"). + ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN types ELSE next_types END AS types"). + Where("(NOT ((types @> next_types) AND (types <@ next_types))) OR next_tx_lt IS NULL"). + Order("change_tx_lt ASC")). + Scan(ctx, &ret) + if err != nil { + return nil, err + } + + var ( + lastInterfaces *[]abi.ContractName + res = map[uint64][]abi.ContractName{} + ) + for it := range ret { + if lastInterfaces != nil && reflect.DeepEqual(ret[it].Types, *lastInterfaces) { + continue + } + res[ret[it].ChangeTxLT] = ret[it].Types + lastInterfaces = &ret[it].Types + } + + return res, nil +} diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index e2de09e7..997c2b68 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -3,14 +3,17 @@ package account_test import ( "context" "database/sql" + "fmt" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" + "github.com/uptrace/go-clickhouse/ch" "github.com/tonindexer/anton/abi" @@ -189,3 +192,113 @@ func BenchmarkRepository_AddAccounts(b *testing.B) { dropTables(b) }) } + +func TestRepository_GetAllAccountInterfaces(t *testing.T) { + a, a2 := rndm.Address(), rndm.Address() + + var testCases = []struct { + accounts []*core.AccountState + result map[uint64][]abi.ContractName + }{ + { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a2, "1", nil, 10), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "2", nil, 12), + rndm.AddressStateContractWithLT(a2, "1", nil, 10), + rndm.AddressStateContractWithLT(a2, "2", nil, 13), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + 12: {"2"}, + }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "1", nil, 12), + rndm.AddressStateContractWithLT(a, "1", nil, 13), + rndm.AddressStateContractWithLT(a, "1", nil, 14), + rndm.AddressStateContractWithLT(a2, "1", nil, 10), + rndm.AddressStateContractWithLT(a2, "1", nil, 15), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "1", nil, 12), + rndm.AddressStateContractWithLT(a, "2", nil, 13), + rndm.AddressStateContractWithLT(a, "2", nil, 14), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + 13: {"2"}, + }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "1", nil, 12), + rndm.AddressStateContractWithLT(a, "2", nil, 13), + rndm.AddressStateContractWithLT(a, "2", nil, 14), + rndm.AddressStateContractWithLT(a, "3", nil, 15), + rndm.AddressStateContractWithLT(a, "3", nil, 16), + rndm.AddressStateContractWithLT(a, "2", nil, 17), + rndm.AddressStateContractWithLT(a, "2", nil, 18), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + 13: {"2"}, + 15: {"3"}, + 17: {"2"}, + }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "2", nil, 13), + rndm.AddressStateContractWithLT(a, "3", nil, 15), + rndm.AddressStateContractWithLT(a, "2", nil, 17), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + 13: {"2"}, + 15: {"3"}, + 17: {"2"}, + }, + }, + } + + initdb(t) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + for i, tc := range testCases { + dropTables(t) + createTables(t) + + tx, err := pg.Begin() + require.NoError(t, err) + + err = repo.AddAccountStates(ctx, tx, tc.accounts) + require.NoError(t, err) + + err = tx.Commit() + require.NoError(t, err) + + res, err := repo.GetAllAccountInterfaces(ctx, *a) + require.NoError(t, err) + + assert.Equal(t, len(tc.result), len(res), fmt.Sprintf("test number %d", i)) + assert.Equal(t, tc.result, res, fmt.Sprintf("test number %d", i)) + } + + dropTables(t) +} diff --git a/internal/core/rndm/account.go b/internal/core/rndm/account.go index 330a146b..008f4654 100644 --- a/internal/core/rndm/account.go +++ b/internal/core/rndm/account.go @@ -88,6 +88,12 @@ func AddressStateContract(a *addr.Address, t abi.ContractName, minter *addr.Addr return AddressState(a, types, minter) } +func AddressStateContractWithLT(a *addr.Address, t abi.ContractName, minter *addr.Address, lt uint64) *core.AccountState { + acc := AddressStateContract(a, t, minter) + acc.LastTxLT = lt + return acc +} + func AddressStates(a *addr.Address, n int) (ret []*core.AccountState) { for i := 0; i < n; i++ { ret = append(ret, AddressState(a, nil, nil)) From d86d51f8c8e8b3922096ae0f3489e7d239c7f99f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 23:20:44 +0700 Subject: [PATCH 079/186] [repo] account.GetAllAccountInterfaces: add test for edge case --- internal/core/repository/account/account_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index 997c2b68..c7e44862 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -219,6 +219,12 @@ func TestRepository_GetAllAccountInterfaces(t *testing.T) { 11: {"1"}, 12: {"2"}, }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a2, "1", nil, 10), + rndm.AddressStateContractWithLT(a2, "2", nil, 13), + }, + result: map[uint64][]abi.ContractName{}, }, { accounts: []*core.AccountState{ rndm.AddressStateContractWithLT(a, "1", nil, 11), From df18a332f3ccdd88e3eebb08543a9719988a873b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 23:44:49 +0700 Subject: [PATCH 080/186] [repo] account.GetAllAccountInterfaces: add test for edge case with null interfaces --- internal/core/repository/account/account.go | 7 ++++++- internal/core/repository/account/account_test.go | 15 +++++++++++++++ internal/core/rndm/account.go | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index e9a9deef..52c58c99 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -306,7 +306,12 @@ func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address Order("tx_lt ASC")). ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN tx_lt ELSE next_tx_lt END AS change_tx_lt"). ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN types ELSE next_types END AS types"). - Where("(NOT ((types @> next_types) AND (types <@ next_types))) OR next_tx_lt IS NULL"). + Where(` + (NOT ((types @> next_types) AND (types <@ next_types))) OR + (types IS NULL AND next_types IS NOT NULL) OR + (types IS NOT NULL AND next_types IS NULL) OR + next_tx_lt IS NULL + `). Order("change_tx_lt ASC")). Scan(ctx, &ret) if err != nil { diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index c7e44862..263d1b48 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -219,6 +219,21 @@ func TestRepository_GetAllAccountInterfaces(t *testing.T) { 11: {"1"}, 12: {"2"}, }, + }, { + accounts: []*core.AccountState{ + rndm.AddressStateContractWithLT(a, "1", nil, 11), + rndm.AddressStateContractWithLT(a, "", nil, 12), + rndm.AddressStateContractWithLT(a, "2", nil, 13), + rndm.AddressStateContractWithLT(a, "", nil, 14), + rndm.AddressStateContractWithLT(a2, "1", nil, 10), + rndm.AddressStateContractWithLT(a2, "2", nil, 13), + }, + result: map[uint64][]abi.ContractName{ + 11: {"1"}, + 12: nil, + 13: {"2"}, + 14: nil, + }, }, { accounts: []*core.AccountState{ rndm.AddressStateContractWithLT(a2, "1", nil, 10), diff --git a/internal/core/rndm/account.go b/internal/core/rndm/account.go index 008f4654..ecd8dcb8 100644 --- a/internal/core/rndm/account.go +++ b/internal/core/rndm/account.go @@ -91,6 +91,9 @@ func AddressStateContract(a *addr.Address, t abi.ContractName, minter *addr.Addr func AddressStateContractWithLT(a *addr.Address, t abi.ContractName, minter *addr.Address, lt uint64) *core.AccountState { acc := AddressStateContract(a, t, minter) acc.LastTxLT = lt + if t == "" { + acc.Types = nil + } return acc } From e2cdea536f317b22c299280f44e6ad9fb24cd872 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 15 Feb 2024 23:53:14 +0700 Subject: [PATCH 081/186] [rescan] rewrite account matching for message parsing --- internal/app/rescan/account.go | 22 ++++------ internal/app/rescan/cache.go | 73 ++++++++++++++++++++++++++++++++ internal/app/rescan/rescan.go | 4 ++ internal/app/rescan/tx.go | 77 +++++++++++++++++++++++----------- 4 files changed, 138 insertions(+), 38 deletions(-) create mode 100644 internal/app/rescan/cache.go diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 5417b6ac..74e8422a 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -16,7 +16,7 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (s *Service) getRecentAccountState(ctx context.Context, master, b core.BlockID, a addr.Address, afterBlock bool) (*core.AccountState, error) { +func (s *Service) getRecentAccountState(ctx context.Context, master, b core.BlockID, a addr.Address) (*core.AccountState, error) { defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) var boundBlock core.BlockID @@ -30,19 +30,13 @@ func (s *Service) getRecentAccountState(ctx context.Context, master, b core.Bloc } accountReq := filter.AccountsReq{ - Addresses: []*addr.Address{&a}, - Workchain: &boundBlock.Workchain, - Shard: &boundBlock.Shard, - Limit: 1, + Addresses: []*addr.Address{&a}, + Workchain: &boundBlock.Workchain, + Shard: &boundBlock.Shard, + BlockSeqNoLeq: &boundBlock.SeqNo, + Order: "DESC", + Limit: 1, } - if afterBlock { - accountReq.BlockSeqNoBeq = &boundBlock.SeqNo - accountReq.Order = "ASC" - } else { - accountReq.BlockSeqNoLeq = &boundBlock.SeqNo - accountReq.Order = "DESC" - } - accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) if err != nil { return nil, errors.Wrap(err, "filter accounts") @@ -66,7 +60,7 @@ func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core. } getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - return s.getRecentAccountState(ctx, master.ID(), b.ID(), a, false) + return s.getRecentAccountState(ctx, master.ID(), b.ID(), a) } update := *tx.Account diff --git a/internal/app/rescan/cache.go b/internal/app/rescan/cache.go new file mode 100644 index 00000000..cff31d65 --- /dev/null +++ b/internal/app/rescan/cache.go @@ -0,0 +1,73 @@ +package rescan + +import ( + "container/list" + "sync" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" +) + +type interfacesCacheItem struct { + data map[uint64][]abi.ContractName + keyPtr *list.Element +} + +// accountInterfacesCache implements LRU cache for account interfaces. +// For a given address it stores account interface updates. +// Used only in messages rescan. +type interfacesCache struct { + queue *list.List + items map[addr.Address]*interfacesCacheItem + capacity int + sync.RWMutex +} + +func newInterfacesCache(capacity int) *interfacesCache { + return &interfacesCache{ + queue: list.New(), + items: map[addr.Address]*interfacesCacheItem{}, + capacity: capacity, + } +} + +func (c *interfacesCache) removeItem() { + back := c.queue.Back() + c.queue.Remove(back) + delete(c.items, back.Value.(addr.Address)) //nolint:forcetypeassert // no need +} + +func (c *interfacesCache) updateItem(item *interfacesCacheItem, k addr.Address, v map[uint64][]abi.ContractName) { + item.data = v + c.items[k] = item + c.queue.MoveToFront(item.keyPtr) +} + +func (c *interfacesCache) put(k addr.Address, v map[uint64][]abi.ContractName) { + c.Lock() + defer c.Unlock() + + if item, ok := c.items[k]; !ok { + if c.capacity == len(c.items) { + c.removeItem() + } + c.items[k] = &interfacesCacheItem{ + data: v, + keyPtr: c.queue.PushFront(k), + } + } else { + c.updateItem(item, k, v) // actually it's not used + } +} + +func (c *interfacesCache) get(key addr.Address) (map[uint64][]abi.ContractName, bool) { + c.RLock() + defer c.RUnlock() + + if item, ok := c.items[key]; ok { + c.queue.MoveToFront(item.keyPtr) + return item.data, true + } + + return nil, false +} diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 26be0d06..c9359bed 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -20,6 +20,8 @@ type Service struct { masterShard int64 + interfacesCache *interfacesCache + run bool mx sync.RWMutex wg sync.WaitGroup @@ -35,6 +37,8 @@ func NewService(cfg *app.RescanConfig) *Service { s.Workers = 1 } + s.interfacesCache = newInterfacesCache(16384) // number of addresses + return s } diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index f988238a..96af312e 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -8,11 +8,56 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" ) -func (s *Service) rescanMessage(ctx context.Context, master core.BlockID, msg *core.Message) *core.Message { +func (s *Service) chooseInterfaces(updates map[uint64][]abi.ContractName, txLT uint64) (ret []abi.ContractName) { + if len(updates) == 0 { + return ret + } + + var maxLT uint64 + for updLT, types := range updates { + if updLT <= txLT && updLT > maxLT { + maxLT = updLT + ret = types + } + } + if ret != nil { + return ret + } + + minLT := uint64(1 << 63) + for updLT, types := range updates { + if updLT < minLT { + minLT = updLT + ret = types + } + } + return ret +} + +func (s *Service) getAccountStateForMessage(ctx context.Context, a addr.Address, txLT uint64) *core.AccountState { + interfaceUpdates, ok := s.interfacesCache.get(a) + if ok { + return &core.AccountState{Address: a, Types: s.chooseInterfaces(interfaceUpdates, txLT)} + } + + interfaceUpdates, err := s.AccountRepo.GetAllAccountInterfaces(ctx, a) + if err != nil { + log.Error().Err(err).Str("addr", a.Base64()).Msg("get all account interfaces") + return nil + } + + s.interfacesCache.put(a, interfaceUpdates) + + return &core.AccountState{Address: a, Types: s.chooseInterfaces(interfaceUpdates, txLT)} +} + +func (s *Service) rescanMessage(ctx context.Context, msg *core.Message) *core.Message { // we must get account state's interfaces to properly determine message operation // and to parse message accordingly @@ -23,26 +68,10 @@ func (s *Service) rescanMessage(ctx context.Context, master core.BlockID, msg *c // which was update just after the message was received if msg.SrcState == nil { - msg.SrcState, _ = s.getRecentAccountState(ctx, - master, - core.BlockID{ - Workchain: msg.SrcWorkchain, - Shard: msg.SrcShard, - SeqNo: msg.SrcBlockSeqNo, - }, - msg.SrcAddress, - false) + msg.SrcState = s.getAccountStateForMessage(ctx, msg.SrcAddress, msg.SrcTxLT) } if msg.DstState == nil { - msg.DstState, _ = s.getRecentAccountState(ctx, - master, - core.BlockID{ - Workchain: msg.DstWorkchain, - Shard: msg.DstShard, - SeqNo: msg.DstBlockSeqNo, - }, - msg.DstAddress, - true) + msg.DstState = s.getAccountStateForMessage(ctx, msg.DstAddress, msg.DstTxLT) } update := *msg @@ -69,18 +98,18 @@ func (s *Service) rescanMessage(ctx context.Context, master core.BlockID, msg *c return &update } -func (s *Service) rescanMessagesInBlock(ctx context.Context, master, b *core.Block) (updates []*core.Message) { +func (s *Service) rescanMessagesInBlock(ctx context.Context, b *core.Block) (updates []*core.Message) { for _, tx := range b.Transactions { if tx.InMsg != nil { tx.InMsg.DstState = tx.Account - if got := s.rescanMessage(ctx, master.ID(), tx.InMsg); got != nil { + if got := s.rescanMessage(ctx, tx.InMsg); got != nil { updates = append(updates, got) } } for _, out := range tx.OutMsg { out.SrcState = tx.Account - if got := s.rescanMessage(ctx, master.ID(), out); got != nil { + if got := s.rescanMessage(ctx, out); got != nil { updates = append(updates, got) } } @@ -90,11 +119,11 @@ func (s *Service) rescanMessagesInBlock(ctx context.Context, master, b *core.Blo func (s *Service) rescanMessagesWorker(m *core.Block) (updates []*core.Message) { for _, shard := range m.Shards { - upd := s.rescanMessagesInBlock(context.Background(), m, shard) + upd := s.rescanMessagesInBlock(context.Background(), shard) updates = append(updates, upd...) } - upd := s.rescanMessagesInBlock(context.Background(), m, m) + upd := s.rescanMessagesInBlock(context.Background(), m) updates = append(updates, upd...) return updates From cdf07ae4b73f71a7b79d0c47a590c943da6ed6d5 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 15:35:44 +0700 Subject: [PATCH 082/186] [abi] add address to get method execution --- abi/get_emulator.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 965b7c10..2bf8d911 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -21,6 +21,8 @@ import ( tutlb "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton/nft" "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/tonindexer/anton/addr" ) type VmValue struct { @@ -33,6 +35,8 @@ 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"` From 7ad3fb6bd770d9e0fdbf37233fd6223c0f124900 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 15:36:56 +0700 Subject: [PATCH 083/186] [abi] nft collection get_nft_content: index format bytes --- abi/known/tep62_nft.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abi/known/tep62_nft.json b/abi/known/tep62_nft.json index 7200d91b..ced72153 100644 --- a/abi/known/tep62_nft.json +++ b/abi/known/tep62_nft.json @@ -261,7 +261,8 @@ "arguments": [ { "name": "index", - "stack_type": "int" + "stack_type": "int", + "format": "bytes" }, { "name": "individual_content", From 3f4661bf1d96f79600e237d1f78eef299ce7e4a7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 15:42:03 +0700 Subject: [PATCH 084/186] [repo] account: gocyclo nolint for filterAccountStates --- 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 c8065e3b..da39cad7 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -76,7 +76,7 @@ func (r *Repository) FilterLabels(ctx context.Context, f *filter.LabelsReq) (*fi return res, nil } -func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { +func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { //nolint:gocyclo // that's ok var ( q *bun.SelectQuery prefix, statesTable string From 72e1e54ef4f76261c70cd8946083a6fd96f5bb74 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 15:42:23 +0700 Subject: [PATCH 085/186] [abi] get_emulator.go: fix import shadowing --- abi/get_emulator.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 2bf8d911..f6218547 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -53,8 +53,8 @@ type Emulator struct { AccountID tongo.AccountID } -func newEmulator(addr *address.Address, e *tvm.Emulator) (*Emulator, error) { - accId, err := ton.AccountIDFromBase64Url(addr.String()) +func newEmulator(a *address.Address, e *tvm.Emulator) (*Emulator, error) { + accId, err := ton.AccountIDFromBase64Url(a.String()) if err != nil { return nil, errors.Wrap(err, "parse address") } @@ -62,7 +62,7 @@ func newEmulator(addr *address.Address, e *tvm.Emulator) (*Emulator, error) { return &Emulator{Emulator: e, AccountID: accId}, nil } -func NewEmulator(addr *address.Address, code, data, cfg *cell.Cell) (*Emulator, error) { +func NewEmulator(a *address.Address, code, data, cfg *cell.Cell) (*Emulator, error) { e, err := tvm.NewEmulatorFromBOCsBase64( base64.StdEncoding.EncodeToString(code.ToBOC()), base64.StdEncoding.EncodeToString(data.ToBOC()), @@ -71,10 +71,10 @@ func NewEmulator(addr *address.Address, code, data, cfg *cell.Cell) (*Emulator, if err != nil { return nil, err } - return newEmulator(addr, e) + return newEmulator(a, e) } -func NewEmulatorBase64(addr *address.Address, code, data, cfg, libraries string) (*Emulator, error) { +func NewEmulatorBase64(a *address.Address, code, data, cfg, libraries string) (*Emulator, error) { var ( e *tvm.Emulator err error @@ -97,7 +97,7 @@ func NewEmulatorBase64(addr *address.Address, code, data, cfg, libraries string) return nil, err } - return newEmulator(addr, e) + return newEmulator(a, e) } func vmMakeValueInt(v *VmValue) (ret tlb.VmStackValue, _ error) { From 9677503ae17887e9bda4a63595a3062165cb716f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 16:19:40 +0700 Subject: [PATCH 086/186] [rescan] add time track to filterBlocksForRescan --- internal/app/rescan/rescan.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index c9359bed..60505e03 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -169,6 +169,8 @@ func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withMessages *req.Shard = s.masterShard *req.AfterSeqNo = fromBlock - 1 + defer app.TimeTrack(time.Now(), "filterBlocksForRescan(%d, %d, %t)", fromBlock-1, workers, withMessages) + res, err := s.BlockRepo.FilterBlocks(context.Background(), req) if err != nil { return nil, err From 7f8d8bf98a1b1a824d57141448249cf5ccad6a76 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 16:50:28 +0700 Subject: [PATCH 087/186] [rescan] do not execute already executed get-methods with same description --- internal/app/parser/account.go | 4 +- internal/app/parser/get.go | 306 ++++++++++++++++++++++++++------ internal/app/parser/get_test.go | 267 ++++++++++++++++++++++++++++ 3 files changed, 521 insertions(+), 56 deletions(-) create mode 100644 internal/app/parser/get_test.go diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index f60dce46..ef0700e9 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -115,7 +115,9 @@ func (s *Service) ParseAccountData( for _, i := range interfaces { acc.Types = append(acc.Types, i.Name) } - acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} + if acc.ExecutedGetMethods == nil { + acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} + } s.callPossibleGetMethods(ctx, acc, others, interfaces) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 6425a387..d9d5d21e 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -1,10 +1,12 @@ package parser import ( + "bytes" "context" "encoding/base64" "fmt" "math/big" + "reflect" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -105,6 +107,153 @@ func (s *Service) callGetMethodNoArgs(ctx context.Context, i *core.ContractInter return stack, nil } +func (s *Service) checkPrevGetMethodExecutionArgs(argsDesc []abi.VmValueDesc, args, prevArgs []any) bool { //nolint:gocognit,gocyclo // that's ok + if len(argsDesc) != len(args) || len(args) != len(prevArgs) { + return false + } + + for it := range argsDesc { + argDesc := &argsDesc[it] + + switch argDesc.StackType { + case "int": + argCasted, ok := args[it].(*big.Int) + if !ok { + return false + } + + var prevArgBI *big.Int + switch argDesc.Format { + case "": + switch prevArgCasted := prevArgs[it].(type) { + case string: + prevArgBI, ok = new(big.Int).SetString(prevArgCasted, 10) + if !ok { + return false + } + case float64: + prevArgBI = big.NewInt(int64(prevArgCasted)) + default: + return false + } + + case "bytes": + prevArgCasted, ok := prevArgs[it].(string) + if !ok { + return false + } + prevArgCastedBytes, err := base64.StdEncoding.DecodeString(prevArgCasted) + if err != nil { + return false + } + prevArgBI = new(big.Int).SetBytes(prevArgCastedBytes) + + default: + return false + } + + if argCasted.Cmp(prevArgBI) != 0 { + return false + } + + case "slice": + if argDesc.Format != "addr" { + return false + } + + argCasted, ok := args[it].(*address.Address) + if !ok { + return false + } + + prevArgCasted, ok := prevArgs[it].(string) + if !ok { + return false + } + prevArgAddr, err := new(addr.Address).FromBase64(prevArgCasted) + if err != nil { + return false + } + + if !addr.Equal(prevArgAddr, addr.MustFromTonutils(argCasted)) { + return false + } + + case "cell": + if argDesc.Format != "" { + return false + } + + argCasted, ok := args[it].(*cell.Cell) + if !ok { + return false + } + + prevArgCasted, ok := prevArgs[it].(string) + if !ok { + return false + } + prevArgBytes, err := base64.StdEncoding.DecodeString(prevArgCasted) + if err != nil { + return false + } + prevArgCell, err := cell.FromBOC(prevArgBytes) + if err != nil { + return false + } + + if !bytes.Equal(argCasted.Hash(), prevArgCell.Hash()) { + return false + } + } + } + + return true +} + +// checkPrevGetMethodExecution returns true, if get-method was already executed on that account with the same arguments +func (s *Service) checkPrevGetMethodExecution(i abi.ContractName, desc *abi.GetMethodDesc, acc *core.AccountState, args []any) (int, bool) { + if acc.ExecutedGetMethods == nil { + return -1, false + } + + executions, ok := acc.ExecutedGetMethods[i] + if !ok { + return -1, false + } + + for it := range executions { + exec := &executions[it] + + if desc.Name != exec.Name { + continue + } + + prevDesc := &abi.GetMethodDesc{ + Name: exec.Name, + Arguments: exec.Arguments, + ReturnValues: exec.ReturnValues, + } + if !reflect.DeepEqual(prevDesc, desc) { + return it, false + } + if len(args) == 0 && len(exec.Receives) == 0 { + return it, true // no arguments, so return values will be the same on second execution + } + + ok := s.checkPrevGetMethodExecutionArgs(desc.Arguments, args, exec.Receives) + return it, ok + } + + return -1, false +} + +func (s *Service) removePrevGetMethodExecution(i abi.ContractName, it int, acc *core.AccountState) { + executions := acc.ExecutedGetMethods[i] + copy(executions[it:], executions[it+1:]) + acc.ExecutedGetMethods[i] = executions[:len(executions)-1] +} + func mapContentDataNFT(ret *core.AccountState, c any) { if c == nil { return @@ -129,28 +278,41 @@ func mapContentDataNFT(ret *core.AccountState, c any) { } func (s *Service) getNFTItemContent(ctx context.Context, collection *core.AccountState, idx *big.Int, itemContent *cell.Cell, acc *core.AccountState) { - exec, err := s.callGetMethod(ctx, - &abi.GetMethodDesc{ - Name: "get_nft_content", - Arguments: []abi.VmValueDesc{{ - Name: "index", - StackType: "int", - }, { - Name: "individual_content", - StackType: "cell", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "full_content", - StackType: "cell", - Format: "content", - }}, - }, - collection, []any{idx, itemContent}, - ) + desc := &abi.GetMethodDesc{ + Name: "get_nft_content", + Arguments: []abi.VmValueDesc{{ + Name: "index", + StackType: "int", + Format: "bytes", + }, { + Name: "individual_content", + StackType: "cell", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "full_content", + StackType: "cell", + Format: "content", + }}, + } + + args := []any{idx.Bytes(), itemContent} + + it, valid := s.checkPrevGetMethodExecution(known.NFTCollection, desc, acc, args) + if valid { + return // old get-method execution is valid + } + if it != -1 { + s.removePrevGetMethodExecution(known.NFTCollection, it, acc) + } + + exec, err := s.callGetMethod(ctx, desc, collection, args) if err != nil { log.Error().Err(err).Msg("execute get_nft_content nft_collection get-method") return } + + exec.Address = &collection.Address + acc.ExecutedGetMethods[known.NFTCollection] = append(acc.ExecutedGetMethods[known.NFTCollection], exec) if exec.Error != "" { return @@ -160,29 +322,40 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun } func (s *Service) checkNFTMinter(ctx context.Context, collection *core.AccountState, idx *big.Int, itemAcc *core.AccountState) { + desc := &abi.GetMethodDesc{ + Name: "get_nft_address_by_index", + Arguments: []abi.VmValueDesc{{ + Name: "index", + StackType: "int", + Format: "bytes", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "address", + StackType: "slice", + Format: "addr", + }}, + } + + args := []any{idx.Bytes()} + + it, valid := s.checkPrevGetMethodExecution(known.NFTCollection, desc, itemAcc, args) + if valid { + return // old get-method execution is valid + } + if it != -1 { + s.removePrevGetMethodExecution(known.NFTCollection, it, itemAcc) + } + itemAcc.Fake = true - exec, err := s.callGetMethod(ctx, - &abi.GetMethodDesc{ - Name: "get_nft_address_by_index", - Arguments: []abi.VmValueDesc{{ - Name: "index", - StackType: "int", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "address", - StackType: "slice", - Format: "addr", - }}, - }, - collection, - []any{idx}, - ) + exec, err := s.callGetMethod(ctx, desc, collection, args) if err != nil { log.Error().Err(err).Msg("execute get_nft_address_by_index nft_collection get-method") return } + exec.Address = &collection.Address + itemAcc.ExecutedGetMethods[known.NFTCollection] = append(itemAcc.ExecutedGetMethods[known.NFTCollection], exec) if exec.Error != "" { log.Error().Err(err).Msg("execute get_nft_address_by_index nft_collection get-method") @@ -196,30 +369,40 @@ func (s *Service) checkNFTMinter(ctx context.Context, collection *core.AccountSt } func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountState, ownerAddr *addr.Address, walletAcc *core.AccountState) { + desc := &abi.GetMethodDesc{ + Name: "get_wallet_address", + Arguments: []abi.VmValueDesc{{ + Name: "owner_address", + StackType: "slice", + Format: "addr", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "wallet_address", + StackType: "slice", + Format: "addr", + }}, + } + + args := []any{ownerAddr.MustToTonutils()} + + it, valid := s.checkPrevGetMethodExecution(known.JettonMinter, desc, walletAcc, args) + if valid { + return // old get-method execution is valid + } + if it != -1 { + s.removePrevGetMethodExecution(known.JettonMinter, it, walletAcc) + } + walletAcc.Fake = true - exec, err := s.callGetMethod(ctx, - &abi.GetMethodDesc{ - Name: "get_wallet_address", - Arguments: []abi.VmValueDesc{{ - Name: "owner_address", - StackType: "slice", - Format: "addr", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "wallet_address", - StackType: "slice", - Format: "addr", - }}, - }, - minter, - []any{ownerAddr.MustToTonutils()}, - ) + exec, err := s.callGetMethod(ctx, desc, minter, args) if err != nil { log.Error().Err(err).Msg("execute get_wallet_address jetton_minter get-method") return } + exec.Address = &minter.Address + walletAcc.ExecutedGetMethods[known.JettonMinter] = append(walletAcc.ExecutedGetMethods[known.JettonMinter], exec) if exec.Error != "" { log.Error().Err(err).Msg("execute get_wallet_address jetton_minter get-method") @@ -246,10 +429,23 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo continue } - exec, err := s.callGetMethodNoArgs(ctx, i, d.Name, acc) - if err != nil { - log.Error().Err(err).Str("contract_name", string(i.Name)).Str("get_method", d.Name).Msg("execute get-method") - return + var ( + exec abi.GetMethodExecution + err error + ) + + id, valid := s.checkPrevGetMethodExecution(i.Name, d, acc, nil) + if valid { + exec = acc.ExecutedGetMethods[i.Name][id] + } else { + if id != -1 { + s.removePrevGetMethodExecution(i.Name, id, acc) + } + exec, err = s.callGetMethodNoArgs(ctx, i, d.Name, acc) + if err != nil { + log.Error().Err(err).Str("contract_name", string(i.Name)).Str("get_method", d.Name).Msg("execute get-method") + return + } } acc.ExecutedGetMethods[i.Name] = append(acc.ExecutedGetMethods[i.Name], exec) diff --git a/internal/app/parser/get_test.go b/internal/app/parser/get_test.go new file mode 100644 index 00000000..0c07d596 --- /dev/null +++ b/internal/app/parser/get_test.go @@ -0,0 +1,267 @@ +package parser + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xssnick/tonutils-go/tvm/cell" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/core" +) + +func mustFromB64(t *testing.T, b64 string) []byte { + s, err := base64.StdEncoding.DecodeString(b64) + require.NoError(t, err) + return s +} + +func mustFromBOC(t *testing.T, b64 string) *cell.Cell { + s, err := base64.StdEncoding.DecodeString(b64) + require.NoError(t, err) + c, err := cell.FromBOC(s) + require.NoError(t, err) + return c +} + +func TestService_checkPrevGetMethodExecution(t *testing.T) { + var testCases = []struct { + contract abi.ContractName + descJson string + executedJson string + args []any + result bool + }{ + { + contract: "nft_collection", + descJson: ` +{ + "name": "get_nft_content", + "arguments": [ + { + "name": "index", + "stack_type": "int" + }, + { + "name": "individual_content", + "stack_type": "cell" + } + ], + "return_values": [ + { + "name": "full_content", + "stack_type": "cell", + "format": "content" + } + ] +}`, + executedJson: ` +{ + "name": "get_nft_content", + "arguments": [ + { + "name": "index", + "stack_type": "int" + }, + { + "name": "individual_content", + "stack_type": "cell" + } + ], + "receives": [ + 1.11966e+5, + "te6cckEBAQEAMwAAYgFodHRwczovL25mdC5mcmFnbWVudC5jb20vbnVtYmVyLzg4ODA4MTk1NzU0Lmpzb26DXfO0" + ], + "return_values": [ + { + "name": "full_content", + "stack_type": "cell", + "format": "content" + } + ], + "returns": [ + { + "URI": "https://nft.fragment.com/number/88808195754.json" + } + ] +}`, + args: []any{big.NewInt(111966), mustFromBOC(t, "te6cckEBAQEAMwAAYgFodHRwczovL25mdC5mcmFnbWVudC5jb20vbnVtYmVyLzg4ODA4MTk1NzU0Lmpzb26DXfO0")}, + result: true, + }, + { + contract: "nft_collection", + descJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int", + "format": "bytes" + } + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ] +}`, + executedJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int" + } + ], + "receives": [ + 10 + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ], + "returns": [ + "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" + ] +}`, + args: []any{big.NewInt(10)}, + result: false, + }, + { + contract: "nft_collection", + descJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int", + "format": "bytes" + } + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ] +}`, + executedJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int", + "format": "bytes" + } + ], + "receives": [ + "08tzIyyFysK97F6dtnqpSZkuqqKit6y2gPvelDuXoPQ=" + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ], + "returns": [ + "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" + ] +}`, + args: []any{mustFromB64(t, "08tzIyyFysK97F6dtnqpSZkuqqKit6y2gPvelDuXoPQ=")}, + result: false, + }, + { + contract: "jetton_minter", + descJson: ` +{ + "name": "get_wallet_address", + "arguments": [ + { + "name": "owner_address", + "stack_type": "slice", + "format": "addr" + } + ], + "return_values": [ + { + "name": "wallet_address", + "stack_type": "slice", + "format": "addr" + } + ] +}`, + executedJson: ` +{ + "name": "get_wallet_address", + "arguments": [ + { + "name": "owner_address", + "stack_type": "slice", + "format": "addr" + } + ], + "receives": [ + "EQDwKGXmxr9kV9bxUoi3o4eU9o6onrKw3g2sd57XAZFWV_kw" + ], + "return_values": [ + { + "name": "wallet_address", + "stack_type": "slice", + "format": "addr" + } + ], + "returns": [ + "EQAdeuTxkNGycqRS-MdwRGVtnEjiS1p7quWaA36Q2XlnOa4Q" + ] +}`, + args: []any{addr.MustFromBase64("EQDwKGXmxr9kV9bxUoi3o4eU9o6onrKw3g2sd57XAZFWV_kw").MustToTonutils()}, + result: true, + }, + } + + for it := range testCases { + ts := &testCases[it] + + var ( + desc abi.GetMethodDesc + exec abi.GetMethodExecution + ) + + err := json.Unmarshal([]byte(ts.descJson), &desc) + require.Nil(t, err) + + err = json.Unmarshal([]byte(ts.executedJson), &exec) + require.Nil(t, err) + + id, res := (*Service)(nil).checkPrevGetMethodExecution( + ts.contract, + &desc, + &core.AccountState{ + ExecutedGetMethods: map[abi.ContractName][]abi.GetMethodExecution{ + ts.contract: {exec}, + }, + }, + ts.args, + ) + require.Equal(t, ts.result, res, fmt.Sprintf("test number %d", it)) + if res { + require.Equal(t, 0, id) + } + } +} From abe18df3f099b78d2be804112f8a31e80341f2c0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 17:07:52 +0700 Subject: [PATCH 088/186] [rescan] filterBlocksForRescan: do not make unnecessary joins --- internal/app/rescan/account.go | 11 ++++------- internal/app/rescan/rescan.go | 11 ++++++----- internal/app/rescan/tx.go | 11 ++--------- internal/core/block.go | 5 +++-- internal/core/filter/block.go | 1 + internal/core/repository/block/filter.go | 6 ++++++ 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 74e8422a..0ba52159 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -49,11 +49,8 @@ func (s *Service) getRecentAccountState(ctx context.Context, master, b core.Bloc } func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core.AccountState) { - for _, tx := range b.Transactions { - if tx.Account == nil { - continue - } - if known.IsOnlyWalletInterfaces(tx.Account.Types) { + for _, acc := range b.Accounts { + if known.IsOnlyWalletInterfaces(acc.Types) { // we do not want to emulate wallet get-methods once again, // as there are lots of them, so it takes a lot of CPU usage continue @@ -63,7 +60,7 @@ func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core. return s.getRecentAccountState(ctx, master.ID(), b.ID(), a) } - update := *tx.Account + update := *acc err := s.Parser.ParseAccountData(context.Background(), &update, getOtherAccountFunc) if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { @@ -71,7 +68,7 @@ func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core. continue } - if reflect.DeepEqual(tx.Account, &update) { + if reflect.DeepEqual(acc, &update) { continue } updates = append(updates, &update) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 60505e03..762dc6a3 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -117,7 +117,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { if task.AccountsLastMaster == 0 { task.AccountsLastMaster = task.StartFrom - 1 } - blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock, false) + blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock, true, false) if err != nil { return errors.Wrap(err, "filter blocks for account states rescan") } @@ -132,7 +132,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { if task.MessagesLastMaster == 0 { task.MessagesLastMaster = task.StartFrom - 1 } - blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock, true) + blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock, false, true) if err != nil { return errors.Wrap(err, "filter blocks for messages states rescan") } @@ -148,7 +148,7 @@ func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { return nil } -func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withMessages bool) ([]*core.Block, error) { +func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withAccounts, withMessages bool) ([]*core.Block, error) { workers := s.Workers if delta := int(toBlock-fromBlock) + 1; delta < workers { workers = delta @@ -158,8 +158,9 @@ func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withMessages Workchain: new(int32), Shard: new(int64), WithShards: true, - WithTransactionAccountState: true, - WithTransactions: true, + WithAccountStates: withAccounts, + WithTransactionAccountState: withMessages, + WithTransactions: withMessages, WithTransactionMessages: withMessages, AfterSeqNo: new(uint32), Order: "ASC", diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 96af312e..968a23bf 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -67,12 +67,8 @@ func (s *Service) rescanMessage(ctx context.Context, msg *core.Message) *core.Me // for the destination of the given message, we take the account state of receiver, // which was update just after the message was received - if msg.SrcState == nil { - msg.SrcState = s.getAccountStateForMessage(ctx, msg.SrcAddress, msg.SrcTxLT) - } - if msg.DstState == nil { - msg.DstState = s.getAccountStateForMessage(ctx, msg.DstAddress, msg.DstTxLT) - } + msg.SrcState = s.getAccountStateForMessage(ctx, msg.SrcAddress, msg.SrcTxLT) + msg.DstState = s.getAccountStateForMessage(ctx, msg.DstAddress, msg.DstTxLT) update := *msg @@ -101,14 +97,11 @@ func (s *Service) rescanMessage(ctx context.Context, msg *core.Message) *core.Me func (s *Service) rescanMessagesInBlock(ctx context.Context, b *core.Block) (updates []*core.Message) { for _, tx := range b.Transactions { if tx.InMsg != nil { - tx.InMsg.DstState = tx.Account if got := s.rescanMessage(ctx, tx.InMsg); got != nil { updates = append(updates, got) } } - for _, out := range tx.OutMsg { - out.SrcState = tx.Account if got := s.rescanMessage(ctx, out); got != nil { updates = append(updates, got) } diff --git a/internal/core/block.go b/internal/core/block.go index 47e2e9c9..6a556304 100644 --- a/internal/core/block.go +++ b/internal/core/block.go @@ -37,8 +37,9 @@ type Block struct { MasterID *BlockID `ch:"-" bun:"embed:master_" json:"master,omitempty"` Shards []*Block `ch:"-" bun:"rel:has-many,join:workchain=master_workchain,join:shard=master_shard,join:seq_no=master_seq_no" json:"shards,omitempty"` - TransactionsCount int `ch:"-" bun:"transactions_count,scanonly" json:"transactions_count"` - Transactions []*Transaction `ch:"-" bun:"rel:has-many,join:workchain=workchain,join:shard=shard,join:seq_no=block_seq_no" json:"transactions,omitempty"` + TransactionsCount int `ch:"-" bun:"transactions_count,scanonly" json:"transactions_count"` + Transactions []*Transaction `ch:"-" bun:"rel:has-many,join:workchain=workchain,join:shard=shard,join:seq_no=block_seq_no" json:"transactions,omitempty"` + Accounts []*AccountState `ch:"-" bun:"rel:has-many,join:workchain=workchain,join:shard=shard,join:seq_no=block_seq_no" json:"accounts,omitempty"` // TODO: block info data diff --git a/internal/core/filter/block.go b/internal/core/filter/block.go index 50b8bcab..9600a988 100644 --- a/internal/core/filter/block.go +++ b/internal/core/filter/block.go @@ -13,6 +13,7 @@ type BlocksReq struct { FileHash []byte `form:"file_hash"` WithShards bool // TODO: array of relations as strings + WithAccountStates bool WithTransactionAccountState bool WithTransactions bool `form:"with_transactions"` WithTransactionMessages bool diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index 9631829c..8487830f 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -97,10 +97,16 @@ func (r *Repository) filterBlocks(ctx context.Context, f *filter.BlocksReq) (ret if f.WithTransactions { q = loadTransactions(q, "Shards.", f) } + if f.WithAccountStates { + q = q.Relation("Shards.Accounts") + } } if f.WithTransactions { q = loadTransactions(q, "", f) } + if f.WithAccountStates { + q = q.Relation("Accounts") + } if f.Workchain != nil { q = q.Where("workchain = ?", *f.Workchain) From e95682037dfdb3fce96bbe3400f1e4ad4f79813a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 17:17:54 +0700 Subject: [PATCH 089/186] [parser] fix code duplication for minter address check --- internal/app/parser/get.go | 87 ++++++++++++++------------------------ 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index d9d5d21e..64892316 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -321,53 +321,57 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun mapContentDataNFT(acc, exec.Returns[0]) } -func (s *Service) checkNFTMinter(ctx context.Context, collection *core.AccountState, idx *big.Int, itemAcc *core.AccountState) { - desc := &abi.GetMethodDesc{ - Name: "get_nft_address_by_index", - Arguments: []abi.VmValueDesc{{ - Name: "index", - StackType: "int", - Format: "bytes", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "address", - StackType: "slice", - Format: "addr", - }}, - } - - args := []any{idx.Bytes()} - - it, valid := s.checkPrevGetMethodExecution(known.NFTCollection, desc, itemAcc, args) +func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountState, i abi.ContractName, desc *abi.GetMethodDesc, args []any) { + it, valid := s.checkPrevGetMethodExecution(i, desc, item, args) if valid { return // old get-method execution is valid } if it != -1 { - s.removePrevGetMethodExecution(known.NFTCollection, it, itemAcc) + s.removePrevGetMethodExecution(i, it, item) } - itemAcc.Fake = true + item.Fake = true - exec, err := s.callGetMethod(ctx, desc, collection, args) + exec, err := s.callGetMethod(ctx, desc, minter, args) if err != nil { - log.Error().Err(err).Msg("execute get_nft_address_by_index nft_collection get-method") + log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) return } - exec.Address = &collection.Address + exec.Address = &minter.Address - itemAcc.ExecutedGetMethods[known.NFTCollection] = append(itemAcc.ExecutedGetMethods[known.NFTCollection], exec) + item.ExecutedGetMethods[i] = append(item.ExecutedGetMethods[i], exec) if exec.Error != "" { - log.Error().Err(err).Msg("execute get_nft_address_by_index nft_collection get-method") + log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) return } itemAddr := addr.MustFromTonutils(exec.Returns[0].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - if addr.Equal(itemAddr, &itemAcc.Address) { - itemAcc.Fake = false + if addr.Equal(itemAddr, &item.Address) { + item.Fake = false } } +func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, idx *big.Int, item *core.AccountState) { + desc := &abi.GetMethodDesc{ + Name: "get_nft_address_by_index", + Arguments: []abi.VmValueDesc{{ + Name: "index", + StackType: "int", + Format: "bytes", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "address", + StackType: "slice", + Format: "addr", + }}, + } + + args := []any{idx.Bytes()} + + s.checkMinter(ctx, minter, item, known.NFTCollection, desc, args) +} + func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountState, ownerAddr *addr.Address, walletAcc *core.AccountState) { desc := &abi.GetMethodDesc{ Name: "get_wallet_address", @@ -385,34 +389,7 @@ func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountSta args := []any{ownerAddr.MustToTonutils()} - it, valid := s.checkPrevGetMethodExecution(known.JettonMinter, desc, walletAcc, args) - if valid { - return // old get-method execution is valid - } - if it != -1 { - s.removePrevGetMethodExecution(known.JettonMinter, it, walletAcc) - } - - walletAcc.Fake = true - - exec, err := s.callGetMethod(ctx, desc, minter, args) - if err != nil { - log.Error().Err(err).Msg("execute get_wallet_address jetton_minter get-method") - return - } - - exec.Address = &minter.Address - - walletAcc.ExecutedGetMethods[known.JettonMinter] = append(walletAcc.ExecutedGetMethods[known.JettonMinter], exec) - if exec.Error != "" { - log.Error().Err(err).Msg("execute get_wallet_address jetton_minter get-method") - return - } - - walletAddr := addr.MustFromTonutils(exec.Returns[0].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - if addr.Equal(walletAddr, &walletAcc.Address) { - walletAcc.Fake = false - } + s.checkMinter(ctx, minter, walletAcc, known.JettonMinter, desc, args) } func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too long From e101449f509098d4326244b7a88f6c535fe2f8b6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 17:44:40 +0700 Subject: [PATCH 090/186] [parser] callGetMethod: save arguments and return values description to the database --- internal/app/parser/get.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 64892316..cb3ae5a5 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -62,7 +62,9 @@ func (s *Service) callGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc * retStack, err := e.RunGetMethod(ctx, d.Name, argsStack, d.ReturnValues) ret = abi.GetMethodExecution{ - Name: d.Name, + Name: d.Name, + Arguments: d.Arguments, + ReturnValues: d.ReturnValues, } for i := range argsStack { ret.Receives = append(ret.Receives, argsStack[i].Payload) From f73b59369fc0ef024219bde4862c73ccf5bfc2f6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 17:45:58 +0700 Subject: [PATCH 091/186] [migrations] reindex state: add index for blocks of account states --- .../20240213085742_reindex_state.down.sql | 4 ++- .../20240213085742_reindex_state.up.sql | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/migrations/pgmigrations/20240213085742_reindex_state.down.sql b/migrations/pgmigrations/20240213085742_reindex_state.down.sql index f1b4363e..e547ce5a 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.down.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.down.sql @@ -1,3 +1,5 @@ SET statement_timeout = 0; -DROP TABLE rescan_tasks; \ No newline at end of file +DROP TABLE rescan_tasks; + +DROP INDEX account_states_workchain_shard_block_seq_no_idx; \ No newline at end of file diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql index 0ab13b52..be7d761c 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.up.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -1,18 +1,22 @@ SET statement_timeout = 0; -CREATE SEQUENCE rescan_tasks_id_seq START WITH 1; +BEGIN; + CREATE SEQUENCE rescan_tasks_id_seq START WITH 1; -CREATE TABLE rescan_tasks ( - id integer NOT NULL DEFAULT nextval('rescan_tasks_id_seq'), - finished bool NOT NULL, - start_from_masterchain_seq_no integer NOT NULL, - accounts_last_masterchain_seq_no integer NOT NULL, - accounts_rescan_done boolean NOT NULL, - messages_last_masterchain_seq_no integer NOT NULL, - messages_rescan_done boolean NOT NULL, - CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) -); + CREATE TABLE rescan_tasks ( + id integer NOT NULL DEFAULT nextval('rescan_tasks_id_seq'), + finished bool NOT NULL, + start_from_masterchain_seq_no integer NOT NULL, + accounts_last_masterchain_seq_no integer NOT NULL, + accounts_rescan_done boolean NOT NULL, + messages_last_masterchain_seq_no integer NOT NULL, + messages_rescan_done boolean NOT NULL, + CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) + ); -ALTER SEQUENCE rescan_tasks_id_seq OWNED BY rescan_tasks.id; + ALTER SEQUENCE rescan_tasks_id_seq OWNED BY rescan_tasks.id; -CREATE UNIQUE INDEX ON rescan_tasks (finished) WHERE finished = false; + CREATE UNIQUE INDEX ON rescan_tasks (finished) WHERE finished = false; +COMMIT; + +CREATE INDEX account_states_workchain_shard_block_seq_no_idx ON account_states USING btree (workchain, shard, block_seq_no); From c13eea62e40beb0d936437bb5869f9fcd2253f27 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 18:51:23 +0700 Subject: [PATCH 092/186] [api] message aggregation: add filter on timestamps --- api/http/docs.go | 30 +++++++++++++++++++++++ api/http/swagger.json | 30 +++++++++++++++++++++++ api/http/swagger.yaml | 20 +++++++++++++++ internal/api/http/controller.go | 2 ++ internal/core/aggregate/msg.go | 4 +++ internal/core/repository/msg/aggregate.go | 27 ++++++++++++++------ 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/api/http/docs.go b/api/http/docs.go index 5f924967..9834ee0b 100644 --- a/api/http/docs.go +++ b/api/http/docs.go @@ -603,6 +603,18 @@ const docTemplate = `{ "in": "query", "required": true }, + { + "type": "string", + "description": "from timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "to timestamp", + "name": "to", + "in": "query" + }, { "maximum": 1000000, "type": "integer", @@ -954,6 +966,12 @@ const docTemplate = `{ "abi.GetMethodExecution": { "type": "object", "properties": { + "address": { + "type": "array", + "items": { + "type": "integer" + } + }, "arguments": { "type": "array", "items": { @@ -1428,6 +1446,12 @@ const docTemplate = `{ "last_tx_lt": { "type": "integer" }, + "libraries": { + "type": "array", + "items": { + "type": "integer" + } + }, "minter_address": { "type": "array", "items": { @@ -1492,6 +1516,12 @@ const docTemplate = `{ "core.Block": { "type": "object", "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/core.AccountState" + } + }, "file_hash": { "type": "array", "items": { diff --git a/api/http/swagger.json b/api/http/swagger.json index 2b55b1d0..241504d8 100644 --- a/api/http/swagger.json +++ b/api/http/swagger.json @@ -600,6 +600,18 @@ "in": "query", "required": true }, + { + "type": "string", + "description": "from timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "to timestamp", + "name": "to", + "in": "query" + }, { "maximum": 1000000, "type": "integer", @@ -951,6 +963,12 @@ "abi.GetMethodExecution": { "type": "object", "properties": { + "address": { + "type": "array", + "items": { + "type": "integer" + } + }, "arguments": { "type": "array", "items": { @@ -1425,6 +1443,12 @@ "last_tx_lt": { "type": "integer" }, + "libraries": { + "type": "array", + "items": { + "type": "integer" + } + }, "minter_address": { "type": "array", "items": { @@ -1489,6 +1513,12 @@ "core.Block": { "type": "object", "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/core.AccountState" + } + }, "file_hash": { "type": "array", "items": { diff --git a/api/http/swagger.yaml b/api/http/swagger.yaml index 3c294b0f..56118394 100644 --- a/api/http/swagger.yaml +++ b/api/http/swagger.yaml @@ -15,6 +15,10 @@ definitions: type: object abi.GetMethodExecution: properties: + address: + items: + type: integer + type: array arguments: items: $ref: '#/definitions/abi.VmValueDesc' @@ -334,6 +338,10 @@ definitions: type: array last_tx_lt: type: integer + libraries: + items: + type: integer + type: array minter_address: items: type: integer @@ -377,6 +385,10 @@ definitions: type: object core.Block: properties: + accounts: + items: + $ref: '#/definitions/core.AccountState' + type: array file_hash: items: type: integer @@ -1141,6 +1153,14 @@ paths: name: order_by required: true type: string + - description: from timestamp + in: query + name: from + type: string + - description: to timestamp + in: query + name: to + type: string - default: 25 description: limit in: query diff --git a/internal/api/http/controller.go b/internal/api/http/controller.go index a58f289c..c5933695 100644 --- a/internal/api/http/controller.go +++ b/internal/api/http/controller.go @@ -660,6 +660,8 @@ func (c *Controller) GetMessages(ctx *gin.Context) { // @Produce json // @Param address query string true "address to aggregate by" // @Param order_by query string true "order aggregated by amount or message count" Enums(amount, count) default(amount) +// @Param from query string false "from timestamp" +// @Param to query string false "to timestamp" // @Param limit query int false "limit" default(25) maximum(1000000) // @Success 200 {object} aggregate.MessagesRes // @Router /messages/aggregated [get] diff --git a/internal/core/aggregate/msg.go b/internal/core/aggregate/msg.go index 35a103f7..1ac37b6b 100644 --- a/internal/core/aggregate/msg.go +++ b/internal/core/aggregate/msg.go @@ -2,6 +2,7 @@ package aggregate import ( "context" + "time" "github.com/uptrace/bun/extra/bunbig" @@ -11,6 +12,9 @@ import ( type MessagesReq struct { Address *addr.Address + From time.Time `form:"from"` + To time.Time `form:"to"` + OrderBy string `form:"order_by"` // amount / count Limit int `form:"limit"` } diff --git a/internal/core/repository/msg/aggregate.go b/internal/core/repository/msg/aggregate.go index 85dafabd..24589e4b 100644 --- a/internal/core/repository/msg/aggregate.go +++ b/internal/core/repository/msg/aggregate.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pkg/errors" + "github.com/uptrace/go-clickhouse/ch" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/aggregate" @@ -16,19 +17,29 @@ func (r *Repository) AggregateMessages(ctx context.Context, req *aggregate.Messa return nil, errors.Wrap(core.ErrInvalidArg, "address must be set") } - err := r.ch.NewSelect().Model((*core.Message)(nil)). + addTimestampFilter := func(q *ch.SelectQuery) *ch.SelectQuery { + if !req.From.IsZero() { + q = q.Where("created_at > ?", req.From) + } + if !req.To.IsZero() { + q = q.Where("created_at < ?", req.To) + } + return q + } + + err := addTimestampFilter(r.ch.NewSelect().Model((*core.Message)(nil)). ColumnExpr("count() as recv_count"). ColumnExpr("sum(amount) as recv_amount"). - Where("dst_address = ?", req.Address). + Where("dst_address = ?", req.Address)). Scan(ctx, &res.RecvCount, &res.RecvAmount) if err != nil { return nil, errors.Wrap(err, "received total") } - err = r.ch.NewSelect().Model((*core.Message)(nil)). + err = addTimestampFilter(r.ch.NewSelect().Model((*core.Message)(nil)). ColumnExpr("count() as sent_count"). ColumnExpr("sum(amount) as sent_amount"). - Where("src_address = ?", req.Address). + Where("src_address = ?", req.Address)). Scan(ctx, &res.SentCount, &res.SentAmount) if err != nil { return nil, errors.Wrap(err, "sent total") @@ -39,10 +50,10 @@ func (r *Repository) AggregateMessages(ctx context.Context, req *aggregate.Messa ColumnExpr("count() as count"). ColumnExpr("sum(sent_amount) as amount"). TableExpr("(?) as q", - r.ch.NewSelect().Model((*core.Message)(nil)). + addTimestampFilter(r.ch.NewSelect().Model((*core.Message)(nil)). ColumnExpr("src_address"). ColumnExpr("amount as sent_amount"). - Where("dst_address = ?", req.Address)). + Where("dst_address = ?", req.Address))). Group("src_address"). Order(req.OrderBy+" DESC"). Limit(req.Limit). @@ -56,10 +67,10 @@ func (r *Repository) AggregateMessages(ctx context.Context, req *aggregate.Messa ColumnExpr("count() as count"). ColumnExpr("sum(sent_amount) as amount"). TableExpr("(?) as q", - r.ch.NewSelect().Model((*core.Message)(nil)). + addTimestampFilter(r.ch.NewSelect().Model((*core.Message)(nil)). ColumnExpr("dst_address"). ColumnExpr("amount as sent_amount"). - Where("src_address = ?", req.Address)). + Where("src_address = ?", req.Address))). Group("dst_address"). Order(req.OrderBy+" DESC"). Limit(req.Limit). From 17cac9caec292da484e3937c2e1d654b4082a8a9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 19:45:16 +0700 Subject: [PATCH 093/186] [parser] callPossibleGetMethods: properly get item index and content from previous get-method execution --- internal/app/parser/get.go | 96 ++++++++++++++++++++++++++++----- internal/app/parser/get_test.go | 45 ++++++++++++++++ 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index cb3ae5a5..fa5911ff 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -394,7 +394,53 @@ func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountSta s.checkMinter(ctx, minter, walletAcc, known.JettonMinter, desc, args) } -func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too long +func (s *Service) prevGetMethodExecutionGetItemParams(exec *abi.GetMethodExecution) (index *big.Int, individualContent *cell.Cell, err error) { + var ok bool + + if len(exec.Returns) < 5 { + return nil, nil, fmt.Errorf("not enough return values: %d", len(exec.Returns)) + } + + switch ret := exec.Returns[1].(type) { + case string: + if len(exec.ReturnValues) > 2 && exec.ReturnValues[1].Format == "bytes" { + indexBytes, err := base64.StdEncoding.DecodeString(ret) + if err != nil { + return nil, nil, errors.Wrapf(err, "decode item index b64 from %s", ret) + } + index = new(big.Int).SetBytes(indexBytes) + } else { + index, ok = new(big.Int).SetString(ret, 10) + if !ok { + return nil, nil, errors.Wrapf(err, "cannot set string from %s", ret) + } + } + + case float64: + index = big.NewInt(int64(ret)) + + default: + return nil, nil, fmt.Errorf("cannot convert %s type to item index", reflect.TypeOf(ret)) + } + + switch ret := exec.Returns[4].(type) { + case string: + boc, err := base64.StdEncoding.DecodeString(ret) + if err != nil { + return nil, nil, errors.Wrapf(err, "decode item content b64 from %s", ret) + } + individualContent, err = cell.FromBOC(boc) + if err != nil { + return nil, nil, errors.Wrap(err, "cannot make cell from boc") + } + default: + return nil, nil, fmt.Errorf("cannot convert %s type to item individual content", reflect.TypeOf(ret)) + } + + return index, individualContent, nil +} + +func (s *Service) callPossibleGetMethods( //nolint:gocognit,gocyclo // yeah, it's too long ctx context.Context, acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error), @@ -423,7 +469,7 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo exec, err = s.callGetMethodNoArgs(ctx, i, d.Name, acc) if err != nil { log.Error().Err(err).Str("contract_name", string(i.Name)).Str("get_method", d.Name).Msg("execute get-method") - return + continue } } @@ -434,12 +480,16 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo switch d.Name { case "get_collection_data": - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - mapContentDataNFT(acc, exec.Returns[1]) + if !valid { + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + mapContentDataNFT(acc, exec.Returns[1]) + } case "get_nft_data": - acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[3].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + if !valid { + acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[3].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + } if acc.MinterAddress == nil { continue @@ -449,16 +499,38 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit // yeah, it's too lo log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get nft collection state") return } - s.getNFTItemContent(ctx, collection, exec.Returns[1].(*big.Int), exec.Returns[4].(*cell.Cell), acc) //nolint:forcetypeassert // panic on wrong interface - s.checkNFTMinter(ctx, collection, exec.Returns[1].(*big.Int), acc) //nolint:forcetypeassert // panic on wrong interface + + var ( + index *big.Int + individualContent *cell.Cell + ) + if !valid { + index, individualContent = exec.Returns[1].(*big.Int), exec.Returns[4].(*cell.Cell) //nolint:forcetypeassert // panic on wrong interface + } else { + index, individualContent, err = s.prevGetMethodExecutionGetItemParams(&exec) + if err != nil { + log.Error().Err(err). + Str("address", acc.Address.Base64()). + Str("minter_address", acc.MinterAddress.Base64()). + Msg("cannot get item index and individual content from previous get-method execution") + continue + } + } + + s.getNFTItemContent(ctx, collection, index, individualContent, acc) + s.checkNFTMinter(ctx, collection, index, acc) case "get_jetton_data": - mapContentDataNFT(acc, exec.Returns[3]) + if !valid { + mapContentDataNFT(acc, exec.Returns[3]) + } case "get_wallet_data": - acc.JettonBalance = bunbig.FromMathBig(exec.Returns[0].(*big.Int)) //nolint:forcetypeassert // panic on wrong interface - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[1].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + if !valid { + acc.JettonBalance = bunbig.FromMathBig(exec.Returns[0].(*big.Int)) //nolint:forcetypeassert // panic on wrong interface + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[1].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + } if acc.MinterAddress == nil || acc.OwnerAddress == nil { continue diff --git a/internal/app/parser/get_test.go b/internal/app/parser/get_test.go index 0c07d596..04869f48 100644 --- a/internal/app/parser/get_test.go +++ b/internal/app/parser/get_test.go @@ -142,6 +142,51 @@ func TestService_checkPrevGetMethodExecution(t *testing.T) { { contract: "nft_collection", descJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int" + } + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ] +}`, + executedJson: ` +{ + "name": "get_nft_address_by_index", + "arguments": [ + { + "name": "index", + "stack_type": "int" + } + ], + "receives": [ + 10 + ], + "return_values": [ + { + "name": "address", + "stack_type": "slice", + "format": "addr" + } + ], + "returns": [ + "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" + ] +}`, + args: []any{big.NewInt(10)}, + result: true, + }, + { + contract: "nft_collection", + descJson: ` { "name": "get_nft_address_by_index", "arguments": [ From a847190f4aed687d0344c12cfcec1fe2e1131e11 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 19:45:55 +0700 Subject: [PATCH 094/186] [abi] nft: get_nft_data returns index in bytes format --- abi/known/tep62_nft.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abi/known/tep62_nft.json b/abi/known/tep62_nft.json index ced72153..63188fd2 100644 --- a/abi/known/tep62_nft.json +++ b/abi/known/tep62_nft.json @@ -109,7 +109,8 @@ }, { "name": "index", - "stack_type": "int" + "stack_type": "int", + "format": "bytes" }, { "name": "collection_address", From b0035bc5ab46beae3d82453cbfe46441cdb42d91 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 19:50:17 +0700 Subject: [PATCH 095/186] [parser] callPossibleGetMethods: item index as bytes --- internal/app/parser/get.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index fa5911ff..8f7f8e23 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -505,7 +505,7 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit,gocyclo // yeah, it' individualContent *cell.Cell ) if !valid { - index, individualContent = exec.Returns[1].(*big.Int), exec.Returns[4].(*cell.Cell) //nolint:forcetypeassert // panic on wrong interface + index, individualContent = new(big.Int).SetBytes(exec.Returns[1].([]byte)), exec.Returns[4].(*cell.Cell) //nolint:forcetypeassert // panic on wrong interface } else { index, individualContent, err = s.prevGetMethodExecutionGetItemParams(&exec) if err != nil { From 0b790ee72b347e251e9a440c14b257f5bce309c7 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 16 Feb 2024 20:34:38 +0700 Subject: [PATCH 096/186] [cmd] label: skip already labeled addresses --- cmd/label/label.go | 5 +++++ internal/core/repository/account/account.go | 3 +++ internal/core/repository/account/filter_test.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/cmd/label/label.go b/cmd/label/label.go index 7bbecb46..3e8e5daa 100644 --- a/cmd/label/label.go +++ b/cmd/label/label.go @@ -7,6 +7,7 @@ import ( "github.com/allisson/go-env" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" "github.com/tonindexer/anton/addr" @@ -157,6 +158,10 @@ var Command = &cli.Command{ for _, l := range labels { err := accRepo.AddAddressLabel(ctx.Context, l) + if errors.Is(err, core.ErrAlreadyExists) { + log.Error().Err(err).Str("addr", l.Address.Base64()).Str("name", l.Name).Msg("cannot insert label") + continue + } if err != nil { return errors.Wrapf(err, "%s label", l.Address.String()) } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 52c58c99..3c96ac1f 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -168,6 +168,9 @@ func CreateTables(ctx context.Context, chDB *ch.DB, pgDB *bun.DB) error { func (r *Repository) AddAddressLabel(ctx context.Context, label *core.AddressLabel) error { _, err := r.pg.NewInsert().Model(label).Exec(ctx) if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return errors.Wrap(core.ErrAlreadyExists, "address is already labeled") + } return errors.Wrap(err, "pg insert label") } _, err = r.ch.NewInsert().Model(label).Exec(ctx) diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index f9b33c86..82e4a02a 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -45,6 +45,10 @@ func TestRepository_FilterLabels(t *testing.T) { err := repo.AddAddressLabel(ctx, dead) require.Nil(t, err) + err = repo.AddAddressLabel(ctx, dead) + require.NotNil(t, err) + require.True(t, errors.Is(err, core.ErrAlreadyExists)) + err = repo.AddAddressLabel(ctx, beef) require.Nil(t, err) From ec5949f977f7c36dcda4f1c1ea79fa0bd7ef089e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 20 Feb 2024 21:32:41 +0700 Subject: [PATCH 097/186] [rescan] filterBlocksForRescan: remove transaction account states join --- internal/app/rescan/rescan.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 762dc6a3..bfcec078 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -155,16 +155,15 @@ func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withAccounts, } req := &filter.BlocksReq{ - Workchain: new(int32), - Shard: new(int64), - WithShards: true, - WithAccountStates: withAccounts, - WithTransactionAccountState: withMessages, - WithTransactions: withMessages, - WithTransactionMessages: withMessages, - AfterSeqNo: new(uint32), - Order: "ASC", - Limit: workers, + Workchain: new(int32), + Shard: new(int64), + WithShards: true, + WithAccountStates: withAccounts, + WithTransactions: withMessages, + WithTransactionMessages: withMessages, + AfterSeqNo: new(uint32), + Order: "ASC", + Limit: workers, } *req.Workchain = -1 *req.Shard = s.masterShard From 760fc7b5cf7cea57c7e2eea40ccb888347941dd4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 21 Feb 2024 20:40:28 +0700 Subject: [PATCH 098/186] [repo] block.filterBlocks: add error wrapping for transactions count --- internal/core/repository/block/filter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/block/filter.go b/internal/core/repository/block/filter.go index 8487830f..5f1871f5 100644 --- a/internal/core/repository/block/filter.go +++ b/internal/core/repository/block/filter.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/pkg/errors" "github.com/uptrace/bun" "github.com/tonindexer/anton/internal/core" @@ -55,7 +56,7 @@ func (r *Repository) countTransactions(ctx context.Context, ret []*core.Block) e Group("workchain", "shard", "block_seq_no"). Scan(ctx, &res) if err != nil { - return err + return errors.Wrap(err, "count transactions for each block") } var counts = make(map[int32]map[int64]map[uint32]int) From b52927168b6006899afafe497534af68508b3a6e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 1 Mar 2024 00:58:36 +0700 Subject: [PATCH 099/186] [repo] GetAllAccountInterfaces through clickhouse --- go.mod | 16 ++--- go.sum | 32 +++++----- internal/core/repository/account/account.go | 59 ++++++++++--------- .../core/repository/account/account_test.go | 4 +- 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/go.mod b/go.mod index 90f2bf6e..89353d67 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/tonindexer/anton go 1.19 -replace github.com/uptrace/go-clickhouse v0.3.0 => github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0 // go-clickhouse branch with dirty fixes +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 require ( github.com/allisson/go-env v0.3.0 @@ -11,7 +11,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.0 github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.3 github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.8.10 @@ -20,7 +20,7 @@ require ( github.com/uptrace/bun/dialect/pgdialect v1.1.12 github.com/uptrace/bun/driver/pgdriver v1.1.12 github.com/uptrace/bun/extra/bunbig v1.1.13-0.20230308071428-7cd855e64a02 - github.com/uptrace/go-clickhouse v0.3.0 + github.com/uptrace/go-clickhouse v0.3.1 github.com/urfave/cli/v2 v2.25.1 github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7 ) @@ -52,7 +52,7 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect @@ -67,13 +67,13 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.opentelemetry.io/otel v1.13.0 // indirect - go.opentelemetry.io/otel/trace v1.13.0 // indirect + 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.6.0 // indirect - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 8b625795..89af68ac 100644 --- a/go.sum +++ b/go.sum @@ -25,7 +25,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 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= @@ -66,8 +66,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 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-20230531081532-4d11768422f0 h1:0/RkLzp6whu/zFCBlEgQyAkbj4QbOlObn8jYQuhm4vg= -github.com/iam047801/go-clickhouse v0.0.0-20230531081532-4d11768422f0/go.mod h1:ZkFYp+b3tn7YiHR6yMnHqGetPfFZhbVYVTsTGBIbdCY= +github.com/iam047801/go-clickhouse v0.0.0-20240229162752-6a94cfc6c817 h1:paJ2keiVrkQme/eSn0w7+N3HuPJFASkuXOGGNpuvQJU= +github.com/iam047801/go-clickhouse v0.0.0-20240229162752-6a94cfc6c817/go.mod h1:h2bP/C3vV5HOMzuA0DZB44ePwpKeUCump86IXlIijkM= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -98,8 +98,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= @@ -146,8 +146,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4= github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc= @@ -184,16 +185,14 @@ 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.8.7 h1:z6NxKNqDVbhS3lyAq2g3XHZhW+/d/DQsnYMBiTN84H0= -github.com/xssnick/tonutils-go v1.8.7/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7 h1:VNEBUjuPRk8pba7TR27nBI1YHF3oqrxBNEl2/ux0Yms= github.com/xssnick/tonutils-go v1.8.8-0.20231205084433-c884d708cbd7/go.mod h1:rqfQ4jsLaFhUUvouz2hTTC02nQGszOhSps7tGAKRC8g= 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.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= -go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg= -go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY= -go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -202,8 +201,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +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.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= @@ -236,8 +235,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 3c96ac1f..96b950b3 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -285,37 +285,38 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address) (map[uint64][]abi.ContractName, error) { var ret []struct { - ChangeTxLT uint64 - Types []abi.ContractName `bun:"type:text[],array"` + ChangeTxLT int64 + ChangeTypes []abi.ContractName `ch:"type:Array(String)" ` } - minTxLtSubQ := r.pg.NewSelect().Model((*core.AccountState)(nil)). + minTxLtSubQ := r.ch.NewSelect().Model((*core.AccountState)(nil)). ColumnExpr("min(last_tx_lt)"). Where("address = ?", &a) - err := r.pg.NewSelect().Model((*core.AccountState)(nil)). - ColumnExpr("last_tx_lt AS change_tx_lt"). - ColumnExpr("types"). - Where("address = ? AND last_tx_lt = (?)", &a, minTxLtSubQ). - UnionAll( - r.pg.NewSelect(). - TableExpr("(?) AS diff", - r.pg.NewSelect().Model((*core.AccountState)(nil)). - ColumnExpr("last_tx_lt AS tx_lt"). - ColumnExpr("types"). - ColumnExpr("lead(last_tx_lt) OVER (ORDER BY last_tx_lt ASC) AS next_tx_lt"). - ColumnExpr("lead(types) OVER (ORDER BY last_tx_lt ASC) AS next_types"). - Where("address = ?", &a). - Order("tx_lt ASC")). - ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN tx_lt ELSE next_tx_lt END AS change_tx_lt"). - ColumnExpr("CASE WHEN next_tx_lt IS NULL THEN types ELSE next_types END AS types"). - Where(` - (NOT ((types @> next_types) AND (types <@ next_types))) OR - (types IS NULL AND next_types IS NOT NULL) OR - (types IS NOT NULL AND next_types IS NULL) OR - next_tx_lt IS NULL - `). - Order("change_tx_lt ASC")). + err := r.ch.NewSelect(). + TableExpr("(?) AS sq", r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS change_tx_lt"). + ColumnExpr("types AS change_types"). + Where("address = ? AND last_tx_lt = (?)", &a, minTxLtSubQ). + UnionAll( + r.ch.NewSelect(). + TableExpr("(?) AS diff", + r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS tx_lt"). + ColumnExpr("types"). + ColumnExpr("leadInFrame(last_tx_lt) OVER (ORDER BY last_tx_lt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_tx_lt"). + ColumnExpr("leadInFrame(types) OVER (ORDER BY last_tx_lt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_types"). + Where("address = ?", &a). + Order("tx_lt ASC")). + ColumnExpr("if(next_tx_lt = 0, tx_lt, next_tx_lt) AS change_tx_lt"). + ColumnExpr("if(next_tx_lt = 0, types, next_types) AS change_types"). + Where(` + (NOT (hasAll(types, next_types) AND hasAll(types, next_types))) OR + (length(types) = 0 AND length(next_types) != 0) OR + (length(types) != 0 AND length(next_types) = 0) OR + next_tx_lt = 0`). + Order("change_tx_lt ASC"))). + Order("change_tx_lt ASC"). Scan(ctx, &ret) if err != nil { return nil, err @@ -326,11 +327,11 @@ func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address res = map[uint64][]abi.ContractName{} ) for it := range ret { - if lastInterfaces != nil && reflect.DeepEqual(ret[it].Types, *lastInterfaces) { + if lastInterfaces != nil && reflect.DeepEqual(ret[it].ChangeTypes, *lastInterfaces) { continue } - res[ret[it].ChangeTxLT] = ret[it].Types - lastInterfaces = &ret[it].Types + res[uint64(ret[it].ChangeTxLT)] = ret[it].ChangeTypes + lastInterfaces = &ret[it].ChangeTypes } return res, nil diff --git a/internal/core/repository/account/account_test.go b/internal/core/repository/account/account_test.go index 263d1b48..579f543f 100644 --- a/internal/core/repository/account/account_test.go +++ b/internal/core/repository/account/account_test.go @@ -230,9 +230,9 @@ func TestRepository_GetAllAccountInterfaces(t *testing.T) { }, result: map[uint64][]abi.ContractName{ 11: {"1"}, - 12: nil, + 12: {}, 13: {"2"}, - 14: nil, + 14: {}, }, }, { accounts: []*core.AccountState{ From fddadd3fdbbb927dd17c15c1db168d2ca8f715c8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Fri, 1 Mar 2024 15:10:24 +0700 Subject: [PATCH 100/186] [abi] emulator: remove c7 register optimization --- abi/get_emulator.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index f6218547..e80fff80 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -63,15 +63,10 @@ func newEmulator(a *address.Address, e *tvm.Emulator) (*Emulator, error) { } func NewEmulator(a *address.Address, code, data, cfg *cell.Cell) (*Emulator, error) { - e, err := tvm.NewEmulatorFromBOCsBase64( + return NewEmulatorBase64(a, base64.StdEncoding.EncodeToString(code.ToBOC()), base64.StdEncoding.EncodeToString(data.ToBOC()), - base64.StdEncoding.EncodeToString(cfg.ToBOC()), - ) - if err != nil { - return nil, err - } - return newEmulator(a, e) + base64.StdEncoding.EncodeToString(cfg.ToBOC()), "") } func NewEmulatorBase64(a *address.Address, code, data, cfg, libraries string) (*Emulator, error) { @@ -80,19 +75,12 @@ func NewEmulatorBase64(a *address.Address, code, data, cfg, libraries string) (* err error ) + args := []tvm.Option{tvm.WithVerbosityLevel(txemulator.PrintsAllStackValuesForCommand)} if libraries != "" { - e, err = tvm.NewEmulatorFromBOCsBase64( - code, - data, - cfg, - tvm.WithLazyC7Optimization(), - tvm.WithLibrariesBase64(libraries), - tvm.WithVerbosityLevel(txemulator.PrintsAllStackValuesForCommand), - ) - } else { - e, err = tvm.NewEmulatorFromBOCsBase64(code, data, cfg, tvm.WithLazyC7Optimization(), tvm.WithVerbosityLevel(txemulator.PrintsAllStackValuesForCommand)) + args = append(args, tvm.WithLibrariesBase64(libraries)) } + e, err = tvm.NewEmulatorFromBOCsBase64(code, data, cfg, args...) if err != nil { return nil, err } From 7dafddc43e6ceff764562344e3b8bde6e303f4fe Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 5 Mar 2024 18:47:11 +0700 Subject: [PATCH 101/186] [repo] account: filter states by contract interface, filter addresses by contract name --- internal/core/account.go | 38 ++++++++++ internal/core/errors.go | 7 +- internal/core/filter/account.go | 2 + internal/core/repository/account/account.go | 72 +++++++++++++++++++ internal/core/repository/account/filter.go | 17 ++++- .../core/repository/account/filter_test.go | 10 +++ 6 files changed, 141 insertions(+), 5 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index cea8578d..6fa5c985 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -50,6 +50,19 @@ type FTWalletData struct { JettonBalance *bunbig.Int `ch:"type:UInt256" bun:"type:numeric" json:"jetton_balance,omitempty" swaggertype:"string"` } +type AccountStateID struct { + Address addr.Address `ch:"type:String"` + LastTxLT uint64 +} + +// AccountStatesInterval is used only GetAddressesByContractName method +// to get minimum and maximum transaction logical time for a given address. +type AccountStatesInterval struct { + Address addr.Address `ch:"type:String"` + MinTxLT uint64 + MaxTxLT uint64 +} + type AccountState struct { ch.CHModel `ch:"account_states,partition:toYYYYMM(updated_at)" json:"-"` bun.BaseModel `bun:"table:account_states" json:"-"` @@ -95,6 +108,14 @@ type AccountState struct { UpdatedAt time.Time `bun:"type:timestamp without time zone,notnull" json:"updated_at"` } +func (a *AccountState) BlockID() BlockID { + return BlockID{ + Workchain: a.Workchain, + Shard: a.Shard, + SeqNo: a.BlockSeqNo, + } +} + type LatestAccountState struct { bun.BaseModel `bun:"table:latest_account_states" json:"-"` @@ -137,6 +158,23 @@ type AccountRepository interface { AddAccountStates(ctx context.Context, tx bun.Tx, states []*AccountState) error UpdateAccountStates(ctx context.Context, states []*AccountState) error + // MatchStatesByInterfaceDesc returns (address, last_tx_lt) pairs for suitable account states. + MatchStatesByInterfaceDesc(ctx context.Context, + contractName abi.ContractName, + addresses []*addr.Address, + codeHash []byte, + getMethodHashes []int32, + afterAddress *addr.Address, + afterTxLt uint64, + limit int) ([]*AccountStateID, error) + + // GetAddressesByContractName returns addresses with matched contract name + // and min(tx_lt), max(tx_lt) of the matched account states. + GetAddressesByContractName(ctx context.Context, + contractName abi.ContractName, + afterAddress *addr.Address, + limit int) ([]*AccountStatesInterval, error) + // GetAllAccountInterfaces returns transaction LT, on which contract interface was updated. // It also considers, that contract can be both upgraded and downgraded. GetAllAccountInterfaces(context.Context, addr.Address) (map[uint64][]abi.ContractName, error) diff --git a/internal/core/errors.go b/internal/core/errors.go index 7d16e611..32cfefe6 100644 --- a/internal/core/errors.go +++ b/internal/core/errors.go @@ -3,7 +3,8 @@ package core import "errors" var ( - ErrNotFound = errors.New("not found") - ErrInvalidArg = errors.New("invalid arguments") - ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") + ErrInvalidArg = errors.New("invalid arguments") + ErrNotImplemented = errors.New("not implemented") + ErrAlreadyExists = errors.New("already exists") ) diff --git a/internal/core/filter/account.go b/internal/core/filter/account.go index 797b16e9..42216ebc 100644 --- a/internal/core/filter/account.go +++ b/internal/core/filter/account.go @@ -24,6 +24,8 @@ type AccountsReq struct { Addresses []*addr.Address // `form:"addresses"` LatestState bool `form:"latest"` + StateIDs []*core.AccountStateID + // filter by block Workchain *int32 Shard *int64 diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 96b950b3..470d214e 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -283,6 +283,78 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A return nil } +func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, + contractName abi.ContractName, + addresses []*addr.Address, + codeHash []byte, + getMethodHashes []int32, + afterAddress *addr.Address, + afterTxLt uint64, + limit int, +) ([]*core.AccountStateID, error) { + var ids []*core.AccountStateID + + q := r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("address"). + ColumnExpr("last_tx_lt"). + WhereGroup(" AND ", func(q *ch.SelectQuery) *ch.SelectQuery { + if contractName != "" { + q = q.WhereOr("contract_name = ?", contractName) + } + if len(addresses) > 0 { + q = q.WhereOr("address IN (?)", addresses) + } + if len(codeHash) > 0 { + q = q.WhereOr("code_hash = ?", codeHash) + } + if len(addresses) == 0 && len(codeHash) == 0 && len(getMethodHashes) > 0 { + // match by get-method hashes only if addresses and code_hash are not set + q = q.WhereOr("hasAll(get_method_hashes, [?])", ch.In(getMethodHashes)) + } + return q + }) + if afterAddress != nil && afterTxLt != 0 { + q = q.Where("(address, after_tx_lt) > (?, ?)", afterAddress, afterTxLt) + } + err := q. + Order("address ASC, last_tx_lt ASC"). + Limit(limit). + Scan(ctx, &ids) + if err != nil { + return nil, err + } + + return ids, nil +} + +// GetAddressesByContractName returns addresses with matched contract name +// and min(tx_lt), max(tx_lt) of the matched account states. +func (r *Repository) GetAddressesByContractName(ctx context.Context, + contractName abi.ContractName, + afterAddress *addr.Address, + limit int, +) ([]*core.AccountStatesInterval, error) { + var intervals []*core.AccountStatesInterval + + q := r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("address"). + ColumnExpr("min(last_tx_lt) as min_tx_lt"). + ColumnExpr("max(last_tx_lt) as max_tx_lt"). + Where("contract_name = ?", contractName) + if afterAddress != nil { + q = q.Where("address > ?", afterAddress) + } + err := q. + Order("address ASC, last_tx_lt ASC"). + Limit(limit). + Scan(ctx, &intervals) + if err != nil { + return nil, err + } + + return intervals, nil +} + func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address) (map[uint64][]abi.ContractName, error) { var ret []struct { ChangeTxLT int64 diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index da39cad7..8801d7cc 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -76,6 +76,13 @@ func (r *Repository) FilterLabels(ctx context.Context, f *filter.LabelsReq) (*fi return res, nil } +func flattenStateIDs(ids []*core.AccountStateID) (ret [][]any) { + for _, id := range ids { + ret = append(ret, []any{&id.Address, id.LastTxLT}) + } + return +} + func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { //nolint:gocyclo // that's ok var ( q *bun.SelectQuery @@ -103,6 +110,9 @@ func (r *Repository) filterAccountStates(ctx context.Context, f *filter.Accounts if len(f.Addresses) > 0 { q = q.Where(statesTable+"address in (?)", bun.In(f.Addresses)) } + if len(f.StateIDs) > 0 { + q = q.Where("("+statesTable+"address, "+statesTable+"last_tx_lt) IN (?)", bun.In(flattenStateIDs(f.StateIDs))) + } if f.Workchain != nil { q = q.Where(prefix+"workchain = ?", *f.Workchain) @@ -172,6 +182,9 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR 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") + } if f.Workchain != nil { q = q.Where("workchain = ?", *f.Workchain) @@ -224,10 +237,10 @@ func (r *Repository) FilterAccounts(ctx context.Context, f *filter.AccountsReq) } res.Total, err = r.countAccountStates(ctx, f) - if err != nil { + if err != nil && !errors.Is(err, core.ErrNotImplemented) { return res, err } - if res.Total == 0 { + if res.Total == 0 && !errors.Is(err, core.ErrNotImplemented) { return res, nil } diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index 82e4a02a..fb882b66 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -258,6 +258,16 @@ func TestRepository_FilterAccounts(t *testing.T) { require.Equal(t, []*core.AccountState{latestState}, results.Rows) }) + t.Run("filter by account state ids", func(t *testing.T) { + results, err := repo.FilterAccounts(ctx, &filter.AccountsReq{ + StateIDs: []*core.AccountStateID{{Address: latestState.Address, LastTxLT: latestState.LastTxLT}}, + Order: "DESC", Limit: 1, + }) + require.Nil(t, err) + require.Equal(t, 0, results.Total) + require.Equal(t, []*core.AccountState{latestState}, results.Rows) + }) + t.Run("drop tables again", func(t *testing.T) { dropTables(t) }) From a92dd81040c1483be12032182e745846f18ce9d0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 5 Mar 2024 19:29:20 +0700 Subject: [PATCH 102/186] [core] update rescan tasks table for smarter rescan --- internal/core/contract.go | 66 +++++++++++++++-- internal/core/repository/contract/contract.go | 47 ++++++++---- .../core/repository/contract/contract_test.go | 72 ++++++++++++++----- 3 files changed, 148 insertions(+), 37 deletions(-) diff --git a/internal/core/contract.go b/internal/core/contract.go index 84db4ccc..dd25bb81 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -38,16 +38,68 @@ type ContractOperation struct { Schema abi.OperationDesc `bun:"type:jsonb" json:"schema"` } +type RescanTaskType string + +const ( + // AddInterface task filters all account states by suitable addresses, code, or get method hashes. + // From these account states, extract (address, last_tx_lt) pairs, + // execute get methods on these pairs, and update the account states with the newly parsed data. + AddInterface RescanTaskType = "add_interface" + + // UpdInterface filters the account states by the already set contract name. + // Again, collect (address, last_tx_lt) pairs, execute get methods, + // update the account states with the parsed data. + UpdInterface RescanTaskType = "upd_interface" + + // DelInterface does the same filtering as UpdInterface, + // but it clears any previously parsed data. + DelInterface RescanTaskType = "del_interface" + + // AddGetMethod task executes this method across all account states that were previously scanned + // and clears all parsed data in account states lacking the new get method. + AddGetMethod RescanTaskType = "add_get_method" + + // DelGetMethod task eliminates the execution of this get-method in all previously parsed account states. + // Then, it includes all account states that match the contract interface description, minus the deleted get method. + DelGetMethod RescanTaskType = "del_get_method" + + // UpdGetMethod task simply iterates through all parsed account states associated with the specified contract name + // and re-execute the changed get method. + UpdGetMethod RescanTaskType = "upd_get_method" + + // UpdOperation task parses contract messages. + // It firstly retrieves all addresses associated with the specified contract name. + // Then, it iterates through all messages directed to (or originating from, in the case of outgoing operations) the current address + // and adds the parsed data. + UpdOperation RescanTaskType = "upd_operation" + + // DelOperation task is the same algorithm, as UpdOperation, but it removes the parsed data. + DelOperation RescanTaskType = "del_operation" +) + type RescanTask struct { bun.BaseModel `bun:"table:rescan_tasks" json:"-"` - ID int `bun:",pk,autoincrement"` - Finished bool `bun:"finished,notnull"` - StartFrom uint32 `bun:"start_from_masterchain_seq_no,notnull"` - AccountsLastMaster uint32 `bun:"accounts_last_masterchain_seq_no,notnull"` - AccountsRescanDone bool `bun:",notnull"` - MessagesLastMaster uint32 `bun:"messages_last_masterchain_seq_no,notnull"` - MessagesRescanDone bool `bun:",notnull"` + ID int `bun:",pk,autoincrement"` + Finished bool `bun:"finished,notnull"` + Type RescanTaskType `bun:"type:rescan_task_type,notnull"` + + // contract being rescanned + ContractName abi.ContractName `bun:",notnull" json:"contract_name"` + Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` + + // for get-method update + ChangedGetMethod string `json:"changed_get_methods,omitempty"` + + // for operations + MessageType MessageType `json:"message_type,omitempty"` + Outgoing bool `json:"outgoing,omitempty"` // if operation is going from contract + OperationID uint32 `json:"operation_id,omitempty"` + Operation *ContractOperation `bun:"rel:has-one,join:contract_name=contract_name,join:outgoing=outgoing,join:operation_id=operation_id" json:"contract_operation"` + + // checkpoint + LastAddress *addr.Address `bun:"type:bytea" json:"last_address"` + LastTxLt uint64 `bun:"type:bigint" json:"last_tx_lt"` } type ContractRepository interface { diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 5643ae13..a09a4636 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -52,10 +52,16 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "contract interface pg create table") } + _, err = pgDB.ExecContext(ctx, "CREATE TYPE rescan_task_type AS ENUM (?, ?, ?, ?, ?, ?, ?, ?)", + core.AddInterface, core.UpdInterface, core.DelInterface, core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod, core.UpdOperation, core.DelOperation) + if err != nil && !strings.Contains(err.Error(), "already exists") { + return errors.Wrap(err, "rescan task type pg create enum") + } + _, err = pgDB.NewCreateTable(). Model(&core.RescanTask{}). IfNotExists(). - WithForeignKeys(). + // WithForeignKeys(). Exec(ctx) if err != nil { return errors.Wrap(err, "rescan task pg create table") @@ -249,14 +255,10 @@ func (r *Repository) GetOperationByID(ctx context.Context, t core.MessageType, i return op, nil } -func (r *Repository) CreateNewRescanTask(ctx context.Context, startFrom uint32) error { - task := core.RescanTask{ - StartFrom: startFrom, - AccountsLastMaster: startFrom - 1, - MessagesLastMaster: startFrom - 1, - } +func (r *Repository) CreateNewRescanTask(ctx context.Context, task *core.RescanTask) error { + task.ID = 0 - _, err := r.pg.NewInsert().Model(&task).Exec(ctx) + _, err := r.pg.NewInsert().Model(task).Exec(ctx) if err != nil { if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { return errors.Wrap(core.ErrAlreadyExists, "cannot create new task while the previous one is unfinished") @@ -276,8 +278,10 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core } err = tx.NewSelect().Model(&task). - Where("finished = ?", false). For("UPDATE"). + Where("finished = ?", false). + Order("id"). + Limit(1). Scan(ctx) if err != nil { _ = tx.Rollback() @@ -287,16 +291,33 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core return bun.Tx{}, nil, err } + if task.Type != core.DelInterface { + task.Contract = new(core.ContractInterface) + err := r.pg.NewSelect().Model(task.Contract). + Where("name = ?", task.ContractName). + Scan(ctx) + if errors.Is(err, sql.ErrNoRows) { + return bun.Tx{}, nil, errors.Wrapf(core.ErrNotFound, "no %s contract interface for %s task", task.ContractName, task.Type) + } + if err != nil { + return bun.Tx{}, nil, err + } + } + if task.Type == core.UpdOperation { + task.Operation, err = r.GetOperationByID(ctx, task.MessageType, []abi.ContractName{task.ContractName}, task.Outgoing, task.OperationID) + if err != nil { + return bun.Tx{}, nil, errors.Wrapf(err, "get 0x%x operation of %s contract for %s task", task.OperationID, task.ContractName, task.Type) + } + } + return tx, &task, nil } func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.RescanTask) error { _, err := tx.NewUpdate().Model(task). Set("finished = ?finished"). - Set("accounts_last_masterchain_seq_no = ?accounts_last_masterchain_seq_no"). - Set("accounts_rescan_done = ?accounts_rescan_done"). - Set("messages_last_masterchain_seq_no = ?messages_last_masterchain_seq_no"). - Set("messages_rescan_done = ?messages_rescan_done"). + Set("last_address = ?last_address"). + Set("last_tx_lt = ?last_tx_lt"). WherePK(). Exec(ctx) if err != nil { diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 79e5c0e4..1b2e5ad5 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -51,19 +51,24 @@ func dropTables(t testing.TB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - _, err := pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) + _, err := pg.NewDropTable().Model((*core.RescanTask)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractInterface)(nil)).IfExists().Exec(ctx) require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractDefinition)(nil)).IfExists().Exec(ctx) require.Nil(t, err) - _, err = pg.NewDropTable().Model((*core.RescanTask)(nil)).IfExists().Exec(ctx) - require.Nil(t, err) _, err = pg.ExecContext(context.Background(), "DROP TYPE message_type") if err != nil && !strings.Contains(err.Error(), "does not exist") { t.Fatal(err) } + + _, err = pg.ExecContext(context.Background(), "DROP TYPE rescan_task_type") + if err != nil && !strings.Contains(err.Error(), "does not exist") { + t.Fatal(err) + } } func TestRepository_AddContracts(t *testing.T) { @@ -235,6 +240,39 @@ func TestRepository_AddContracts(t *testing.T) { func TestRepository_CreateNewRescanTask(t *testing.T) { initdb(t) + i := &core.ContractInterface{ + Name: known.NFTItem, + Addresses: []*addr.Address{rndm.Address()}, + Code: rndm.Bytes(128), + GetMethodsDesc: []abi.GetMethodDesc{ + { + Name: "get_nft_content", + Arguments: []abi.VmValueDesc{ + { + Name: "index", + StackType: "int", + }, { + Name: "individual_content", + StackType: "cell", + }, + }, + ReturnValues: []abi.VmValueDesc{ + { + Name: "full_content", + StackType: "cell", + Format: "content", + }, + }, + }, + }, + GetMethodHashes: rndm.GetMethodHashes(), + } + + task := core.RescanTask{ + Type: core.AddInterface, + ContractName: known.NFTItem, + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() @@ -246,24 +284,22 @@ func TestRepository_CreateNewRescanTask(t *testing.T) { createTables(t) }) - t.Run("create new task", func(t *testing.T) { - err := repo.CreateNewRescanTask(ctx, 10) - require.NoError(t, err) + t.Run("insert interface", func(t *testing.T) { + err := repo.AddInterface(ctx, i) + require.Nil(t, err) }) - t.Run("get 'already exists' error on second creation of unfinished task", func(t *testing.T) { - err := repo.CreateNewRescanTask(ctx, 10) - require.Error(t, err) - require.True(t, errors.Is(err, core.ErrAlreadyExists)) + t.Run("create new task", func(t *testing.T) { + err := repo.CreateNewRescanTask(ctx, &task) + require.NoError(t, err) }) t.Run("update unfinished task", func(t *testing.T) { tx, task, err := repo.GetUnfinishedRescanTask(ctx) require.NoError(t, err) - task.AccountsRescanDone = true - task.AccountsLastMaster = 30 - task.MessagesLastMaster = 20 + task.LastAddress = i.Addresses[0] + task.LastTxLt = 10 err = repo.SetRescanTask(ctx, tx, task) require.NoError(t, err) @@ -272,9 +308,11 @@ func TestRepository_CreateNewRescanTask(t *testing.T) { t.Run("finish task", func(t *testing.T) { tx, task, err := repo.GetUnfinishedRescanTask(ctx) require.NoError(t, err) + require.Equal(t, i.Addresses[0], task.LastAddress) + require.Equal(t, uint64(10), task.LastTxLt) - task.MessagesRescanDone = true - task.MessagesLastMaster = 30 + task.LastAddress = i.Addresses[0] + task.LastTxLt = 20 task.Finished = true err = repo.SetRescanTask(ctx, tx, task) @@ -288,12 +326,12 @@ func TestRepository_CreateNewRescanTask(t *testing.T) { }) t.Run("create second task", func(t *testing.T) { - err := repo.CreateNewRescanTask(ctx, 20) + err := repo.CreateNewRescanTask(ctx, &task) require.NoError(t, err) tx, task, err := repo.GetUnfinishedRescanTask(ctx) require.NoError(t, err) - require.Equal(t, 3, task.ID) + require.Equal(t, 2, task.ID) task.Finished = true From a514e1432b128de55b689d34e858dd89d5d4fa27 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:44:17 +0700 Subject: [PATCH 103/186] [repo] account: remove GetAddressesByContractName method --- internal/core/account.go | 15 ----------- internal/core/repository/account/account.go | 28 --------------------- 2 files changed, 43 deletions(-) diff --git a/internal/core/account.go b/internal/core/account.go index 6fa5c985..321641f7 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -55,14 +55,6 @@ type AccountStateID struct { LastTxLT uint64 } -// AccountStatesInterval is used only GetAddressesByContractName method -// to get minimum and maximum transaction logical time for a given address. -type AccountStatesInterval struct { - Address addr.Address `ch:"type:String"` - MinTxLT uint64 - MaxTxLT uint64 -} - type AccountState struct { ch.CHModel `ch:"account_states,partition:toYYYYMM(updated_at)" json:"-"` bun.BaseModel `bun:"table:account_states" json:"-"` @@ -168,13 +160,6 @@ type AccountRepository interface { afterTxLt uint64, limit int) ([]*AccountStateID, error) - // GetAddressesByContractName returns addresses with matched contract name - // and min(tx_lt), max(tx_lt) of the matched account states. - GetAddressesByContractName(ctx context.Context, - contractName abi.ContractName, - afterAddress *addr.Address, - limit int) ([]*AccountStatesInterval, error) - // GetAllAccountInterfaces returns transaction LT, on which contract interface was updated. // It also considers, that contract can be both upgraded and downgraded. GetAllAccountInterfaces(context.Context, addr.Address) (map[uint64][]abi.ContractName, error) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 470d214e..4303372a 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -327,34 +327,6 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, return ids, nil } -// GetAddressesByContractName returns addresses with matched contract name -// and min(tx_lt), max(tx_lt) of the matched account states. -func (r *Repository) GetAddressesByContractName(ctx context.Context, - contractName abi.ContractName, - afterAddress *addr.Address, - limit int, -) ([]*core.AccountStatesInterval, error) { - var intervals []*core.AccountStatesInterval - - q := r.ch.NewSelect().Model((*core.AccountState)(nil)). - ColumnExpr("address"). - ColumnExpr("min(last_tx_lt) as min_tx_lt"). - ColumnExpr("max(last_tx_lt) as max_tx_lt"). - Where("contract_name = ?", contractName) - if afterAddress != nil { - q = q.Where("address > ?", afterAddress) - } - err := q. - Order("address ASC, last_tx_lt ASC"). - Limit(limit). - Scan(ctx, &intervals) - if err != nil { - return nil, err - } - - return intervals, nil -} - func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address) (map[uint64][]abi.ContractName, error) { var ret []struct { ChangeTxLT int64 From 33c09fc2d4733ed483e4898b9ef5b76a4e454bc6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:44:36 +0700 Subject: [PATCH 104/186] [core] fix UpdOperation rescan task description --- internal/core/contract.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/contract.go b/internal/core/contract.go index dd25bb81..ffbb55d5 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -68,8 +68,8 @@ const ( UpdGetMethod RescanTaskType = "upd_get_method" // UpdOperation task parses contract messages. - // It firstly retrieves all addresses associated with the specified contract name. - // Then, it iterates through all messages directed to (or originating from, in the case of outgoing operations) the current address + // It iterates through all messages with specified operation id, + // directed to (or originating from, in the case of outgoing operations) the given contract // and adds the parsed data. UpdOperation RescanTaskType = "upd_operation" From f1b84a80d12e5d2a5b0a521b09db39ee3469c841 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:45:21 +0700 Subject: [PATCH 105/186] [repo] msg: match messages by operation description --- internal/core/msg.go | 11 +++++ internal/core/repository/msg/msg.go | 69 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/internal/core/msg.go b/internal/core/msg.go index ec7bf492..c63511dd 100644 --- a/internal/core/msg.go +++ b/internal/core/msg.go @@ -82,4 +82,15 @@ type MessageRepository interface { UpdateMessages(ctx context.Context, messages []*Message) error GetMessage(ctx context.Context, hash []byte) (*Message, error) + GetMessages(ctx context.Context, hash [][]byte) ([]*Message, error) + + // MatchMessagesByOperationDesc returns hashes of suitable messages for the given contract operation. + MatchMessagesByOperationDesc(ctx context.Context, + contractName abi.ContractName, + msgType MessageType, + outgoing bool, + operationId uint32, + afterAddress *addr.Address, + afterTxLT uint64, + limit int) ([][]byte, error) } diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 35bea58a..5cd454ae 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -3,6 +3,7 @@ package msg import ( "context" "database/sql" + "fmt" "strings" "github.com/pkg/errors" @@ -10,6 +11,8 @@ import ( "github.com/uptrace/bun" "github.com/uptrace/go-clickhouse/ch" + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository" ) @@ -256,3 +259,69 @@ func (r *Repository) GetMessage(ctx context.Context, hash []byte) (*core.Message return &ret, nil } + +func (r *Repository) GetMessages(ctx context.Context, hashes [][]byte) ([]*core.Message, error) { + var ret []*core.Message + + err := r.pg.NewSelect().Model(&ret). + Where("hash IN (?)", bun.In(hashes)). + Scan(ctx) + if errors.Is(err, sql.ErrNoRows) { + return nil, core.ErrNotFound + } + if err != nil { + return nil, err + } + + return ret, nil +} + +// MatchMessagesByOperationDesc returns hashes of suitable messages for the given contract operation. +func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, + contractName abi.ContractName, + msgType core.MessageType, + outgoing bool, + operationId uint32, + afterAddress *addr.Address, + afterTxLt uint64, + limit int, +) (hashes [][]byte, err error) { + var addresses []*addr.Address + + q := r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("address"). + Where("contract_name = ?", contractName) + if afterAddress != nil { + q = q.Where("address >= ?", afterAddress) + } + err = q. + Order("address ASC"). + Limit(limit). + Scan(ctx, &addresses) + if err != nil { + return nil, errors.Wrap(err, "get contract addresses") + } + + addrCol, ltCol := "dst_address", "dst_tx_lt" + if outgoing { + addrCol, ltCol = "src_address", "src_tx_lt" + } + + q = r.ch.NewSelect().Model((*core.Message)(nil)). + ColumnExpr("hash"). + Where("type = ?", msgType). + Where(addrCol+" IN (?)", bun.In(addresses)). + Where("operation_id = ?", operationId) + if afterAddress != nil && afterTxLt != 0 { + q = q.Where(fmt.Sprintf("(%s, %s) > (?, ?)", addrCol, ltCol), afterAddress, afterTxLt) + } + err = q. + Order(fmt.Sprintf("%s ASC, %s ASC", addrCol, ltCol)). + Limit(limit). + Scan(ctx, &hashes) + if err != nil { + return nil, errors.Wrap(err, "get message hashes") + } + + return hashes, nil +} From 990c853e74e8136cadf5a31edca8c306410f9ab8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:47:35 +0700 Subject: [PATCH 106/186] [parser] remove previous get-method execution check --- internal/app/parser/get.go | 353 +++++++------------------------------ 1 file changed, 60 insertions(+), 293 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 8f7f8e23..bfe9dd0a 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -1,12 +1,10 @@ package parser import ( - "bytes" "context" "encoding/base64" "fmt" "math/big" - "reflect" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -32,7 +30,7 @@ func getMethodByName(i *core.ContractInterface, n string) *abi.GetMethodDesc { return nil } -func (s *Service) callGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc *core.AccountState, args []any) (ret abi.GetMethodExecution, err error) { +func (s *Service) emulateGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc *core.AccountState, args []any) (ret abi.GetMethodExecution, err error) { var argsStack abi.VmStack if len(acc.Code) == 0 || len(acc.Data) == 0 { @@ -62,9 +60,7 @@ func (s *Service) callGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc * retStack, err := e.RunGetMethod(ctx, d.Name, argsStack, d.ReturnValues) ret = abi.GetMethodExecution{ - Name: d.Name, - Arguments: d.Arguments, - ReturnValues: d.ReturnValues, + Name: d.Name, } for i := range argsStack { ret.Receives = append(ret.Receives, argsStack[i].Payload) @@ -90,7 +86,7 @@ func (s *Service) callGetMethod(ctx context.Context, d *abi.GetMethodDesc, acc * return ret, nil } -func (s *Service) callGetMethodNoArgs(ctx context.Context, i *core.ContractInterface, gmName string, acc *core.AccountState) (ret abi.GetMethodExecution, err error) { +func (s *Service) emulateGetMethodNoArgs(ctx context.Context, i *core.ContractInterface, gmName string, acc *core.AccountState) (ret abi.GetMethodExecution, err error) { gm := getMethodByName(i, gmName) if gm == nil { // we panic as contract interface was defined, but there are no standard get-method @@ -101,7 +97,7 @@ func (s *Service) callGetMethodNoArgs(ctx context.Context, i *core.ContractInter panic(fmt.Errorf("%s `%s` get-method has arguments", i.Name, gmName)) } - stack, err := s.callGetMethod(ctx, gm, acc, nil) + stack, err := s.emulateGetMethod(ctx, gm, acc, nil) if err != nil { return ret, errors.Wrapf(err, "%s `%s`", i.Name, gmName) } @@ -109,153 +105,6 @@ func (s *Service) callGetMethodNoArgs(ctx context.Context, i *core.ContractInter return stack, nil } -func (s *Service) checkPrevGetMethodExecutionArgs(argsDesc []abi.VmValueDesc, args, prevArgs []any) bool { //nolint:gocognit,gocyclo // that's ok - if len(argsDesc) != len(args) || len(args) != len(prevArgs) { - return false - } - - for it := range argsDesc { - argDesc := &argsDesc[it] - - switch argDesc.StackType { - case "int": - argCasted, ok := args[it].(*big.Int) - if !ok { - return false - } - - var prevArgBI *big.Int - switch argDesc.Format { - case "": - switch prevArgCasted := prevArgs[it].(type) { - case string: - prevArgBI, ok = new(big.Int).SetString(prevArgCasted, 10) - if !ok { - return false - } - case float64: - prevArgBI = big.NewInt(int64(prevArgCasted)) - default: - return false - } - - case "bytes": - prevArgCasted, ok := prevArgs[it].(string) - if !ok { - return false - } - prevArgCastedBytes, err := base64.StdEncoding.DecodeString(prevArgCasted) - if err != nil { - return false - } - prevArgBI = new(big.Int).SetBytes(prevArgCastedBytes) - - default: - return false - } - - if argCasted.Cmp(prevArgBI) != 0 { - return false - } - - case "slice": - if argDesc.Format != "addr" { - return false - } - - argCasted, ok := args[it].(*address.Address) - if !ok { - return false - } - - prevArgCasted, ok := prevArgs[it].(string) - if !ok { - return false - } - prevArgAddr, err := new(addr.Address).FromBase64(prevArgCasted) - if err != nil { - return false - } - - if !addr.Equal(prevArgAddr, addr.MustFromTonutils(argCasted)) { - return false - } - - case "cell": - if argDesc.Format != "" { - return false - } - - argCasted, ok := args[it].(*cell.Cell) - if !ok { - return false - } - - prevArgCasted, ok := prevArgs[it].(string) - if !ok { - return false - } - prevArgBytes, err := base64.StdEncoding.DecodeString(prevArgCasted) - if err != nil { - return false - } - prevArgCell, err := cell.FromBOC(prevArgBytes) - if err != nil { - return false - } - - if !bytes.Equal(argCasted.Hash(), prevArgCell.Hash()) { - return false - } - } - } - - return true -} - -// checkPrevGetMethodExecution returns true, if get-method was already executed on that account with the same arguments -func (s *Service) checkPrevGetMethodExecution(i abi.ContractName, desc *abi.GetMethodDesc, acc *core.AccountState, args []any) (int, bool) { - if acc.ExecutedGetMethods == nil { - return -1, false - } - - executions, ok := acc.ExecutedGetMethods[i] - if !ok { - return -1, false - } - - for it := range executions { - exec := &executions[it] - - if desc.Name != exec.Name { - continue - } - - prevDesc := &abi.GetMethodDesc{ - Name: exec.Name, - Arguments: exec.Arguments, - ReturnValues: exec.ReturnValues, - } - if !reflect.DeepEqual(prevDesc, desc) { - return it, false - } - if len(args) == 0 && len(exec.Receives) == 0 { - return it, true // no arguments, so return values will be the same on second execution - } - - ok := s.checkPrevGetMethodExecutionArgs(desc.Arguments, args, exec.Receives) - return it, ok - } - - return -1, false -} - -func (s *Service) removePrevGetMethodExecution(i abi.ContractName, it int, acc *core.AccountState) { - executions := acc.ExecutedGetMethods[i] - copy(executions[it:], executions[it+1:]) - acc.ExecutedGetMethods[i] = executions[:len(executions)-1] -} - func mapContentDataNFT(ret *core.AccountState, c any) { if c == nil { return @@ -299,15 +148,7 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun args := []any{idx.Bytes(), itemContent} - it, valid := s.checkPrevGetMethodExecution(known.NFTCollection, desc, acc, args) - if valid { - return // old get-method execution is valid - } - if it != -1 { - s.removePrevGetMethodExecution(known.NFTCollection, it, acc) - } - - exec, err := s.callGetMethod(ctx, desc, collection, args) + exec, err := s.emulateGetMethod(ctx, desc, collection, args) if err != nil { log.Error().Err(err).Msg("execute get_nft_content nft_collection get-method") return @@ -324,17 +165,9 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun } func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountState, i abi.ContractName, desc *abi.GetMethodDesc, args []any) { - it, valid := s.checkPrevGetMethodExecution(i, desc, item, args) - if valid { - return // old get-method execution is valid - } - if it != -1 { - s.removePrevGetMethodExecution(i, it, item) - } - item.Fake = true - exec, err := s.callGetMethod(ctx, desc, minter, args) + exec, err := s.emulateGetMethod(ctx, desc, minter, args) if err != nil { log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) return @@ -394,53 +227,72 @@ func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountSta s.checkMinter(ctx, minter, walletAcc, known.JettonMinter, desc, args) } -func (s *Service) prevGetMethodExecutionGetItemParams(exec *abi.GetMethodExecution) (index *big.Int, individualContent *cell.Cell, err error) { - var ok bool +func (s *Service) callGetMethod( + ctx context.Context, + acc *core.AccountState, + i *core.ContractInterface, + getMethodDesc *abi.GetMethodDesc, + others func(context.Context, addr.Address) (*core.AccountState, error), +) error { + exec, err := s.emulateGetMethodNoArgs(ctx, i, getMethodDesc.Name, acc) + if err != nil { + return errors.Wrapf(err, "execute get-method") + } - if len(exec.Returns) < 5 { - return nil, nil, fmt.Errorf("not enough return values: %d", len(exec.Returns)) + acc.ExecutedGetMethods[i.Name] = append(acc.ExecutedGetMethods[i.Name], exec) + if exec.Error != "" { + return nil } - switch ret := exec.Returns[1].(type) { - case string: - if len(exec.ReturnValues) > 2 && exec.ReturnValues[1].Format == "bytes" { - indexBytes, err := base64.StdEncoding.DecodeString(ret) - if err != nil { - return nil, nil, errors.Wrapf(err, "decode item index b64 from %s", ret) - } - index = new(big.Int).SetBytes(indexBytes) - } else { - index, ok = new(big.Int).SetString(ret, 10) - if !ok { - return nil, nil, errors.Wrapf(err, "cannot set string from %s", ret) - } - } + switch getMethodDesc.Name { + case "get_collection_data": + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + mapContentDataNFT(acc, exec.Returns[1]) - case float64: - index = big.NewInt(int64(ret)) + case "get_nft_data": + acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[3].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - default: - return nil, nil, fmt.Errorf("cannot convert %s type to item index", reflect.TypeOf(ret)) - } + if acc.MinterAddress == nil { + return nil + } - switch ret := exec.Returns[4].(type) { - case string: - boc, err := base64.StdEncoding.DecodeString(ret) + collection, err := others(ctx, *acc.MinterAddress) if err != nil { - return nil, nil, errors.Wrapf(err, "decode item content b64 from %s", ret) + log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get nft collection state") + return nil + } + + index, individualContent := new(big.Int).SetBytes(exec.Returns[1].([]byte)), exec.Returns[4].(*cell.Cell) //nolint:forcetypeassert // panic on wrong interface + + s.getNFTItemContent(ctx, collection, index, individualContent, acc) + s.checkNFTMinter(ctx, collection, index, acc) + + case "get_jetton_data": + mapContentDataNFT(acc, exec.Returns[3]) + + case "get_wallet_data": + acc.JettonBalance = bunbig.FromMathBig(exec.Returns[0].(*big.Int)) //nolint:forcetypeassert // panic on wrong interface + acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[1].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface + + if acc.MinterAddress == nil || acc.OwnerAddress == nil { + return nil } - individualContent, err = cell.FromBOC(boc) + + minter, err := others(ctx, *acc.MinterAddress) if err != nil { - return nil, nil, errors.Wrap(err, "cannot make cell from boc") + log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get jetton minter state") + return nil } - default: - return nil, nil, fmt.Errorf("cannot convert %s type to item individual content", reflect.TypeOf(ret)) + + s.checkJettonMinter(ctx, minter, acc.OwnerAddress, acc) } - return index, individualContent, nil + return nil } -func (s *Service) callPossibleGetMethods( //nolint:gocognit,gocyclo // yeah, it's too long +func (s *Service) callPossibleGetMethods( ctx context.Context, acc *core.AccountState, others func(context.Context, addr.Address) (*core.AccountState, error), @@ -454,93 +306,8 @@ func (s *Service) callPossibleGetMethods( //nolint:gocognit,gocyclo // yeah, it' continue } - var ( - exec abi.GetMethodExecution - err error - ) - - id, valid := s.checkPrevGetMethodExecution(i.Name, d, acc, nil) - if valid { - exec = acc.ExecutedGetMethods[i.Name][id] - } else { - if id != -1 { - s.removePrevGetMethodExecution(i.Name, id, acc) - } - exec, err = s.callGetMethodNoArgs(ctx, i, d.Name, acc) - if err != nil { - log.Error().Err(err).Str("contract_name", string(i.Name)).Str("get_method", d.Name).Msg("execute get-method") - continue - } - } - - acc.ExecutedGetMethods[i.Name] = append(acc.ExecutedGetMethods[i.Name], exec) - if exec.Error != "" { - continue - } - - switch d.Name { - case "get_collection_data": - if !valid { - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - mapContentDataNFT(acc, exec.Returns[1]) - } - - case "get_nft_data": - if !valid { - acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[3].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - } - - if acc.MinterAddress == nil { - continue - } - collection, err := others(ctx, *acc.MinterAddress) - if err != nil { - log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get nft collection state") - return - } - - var ( - index *big.Int - individualContent *cell.Cell - ) - if !valid { - index, individualContent = new(big.Int).SetBytes(exec.Returns[1].([]byte)), exec.Returns[4].(*cell.Cell) //nolint:forcetypeassert // panic on wrong interface - } else { - index, individualContent, err = s.prevGetMethodExecutionGetItemParams(&exec) - if err != nil { - log.Error().Err(err). - Str("address", acc.Address.Base64()). - Str("minter_address", acc.MinterAddress.Base64()). - Msg("cannot get item index and individual content from previous get-method execution") - continue - } - } - - s.getNFTItemContent(ctx, collection, index, individualContent, acc) - s.checkNFTMinter(ctx, collection, index, acc) - - case "get_jetton_data": - if !valid { - mapContentDataNFT(acc, exec.Returns[3]) - } - - case "get_wallet_data": - if !valid { - acc.JettonBalance = bunbig.FromMathBig(exec.Returns[0].(*big.Int)) //nolint:forcetypeassert // panic on wrong interface - acc.OwnerAddress = addr.MustFromTonutils(exec.Returns[1].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - acc.MinterAddress = addr.MustFromTonutils(exec.Returns[2].(*address.Address)) //nolint:forcetypeassert // panic on wrong interface - } - - if acc.MinterAddress == nil || acc.OwnerAddress == nil { - continue - } - minter, err := others(ctx, *acc.MinterAddress) - if err != nil { - log.Error().Str("minter_address", acc.MinterAddress.Base64()).Err(err).Msg("get jetton minter state") - return - } - s.checkJettonMinter(ctx, minter, acc.OwnerAddress, acc) + if err := s.callGetMethod(ctx, acc, i, d, others); err != nil { + log.Error().Err(err).Str("contract_name", string(i.Name)).Str("get_method", d.Name).Msg("execute get-method") } } } From cdd6e70b77aa740cf885574a940bc9c45526e838 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:48:02 +0700 Subject: [PATCH 107/186] [abi] get-method execution: remove arguments and return values description --- abi/get_emulator.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index e80fff80..5709fcd1 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -37,11 +37,8 @@ type GetMethodExecution struct { 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"` + Receives []any `json:"receives,omitempty"` + Returns []any `json:"returns,omitempty"` Error string `json:"error,omitempty"` } From dbe2404cdcdbd549e26a8526711ddcb111f40bbf Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 21:57:14 +0700 Subject: [PATCH 108/186] [parser] add methods for specific get-method execution and specific contract rescan --- internal/app/parser.go | 16 +++++ internal/app/parser/account.go | 119 ++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 16 deletions(-) diff --git a/internal/app/parser.go b/internal/app/parser.go index ed6594f8..8890f4b3 100644 --- a/internal/app/parser.go +++ b/internal/app/parser.go @@ -10,6 +10,7 @@ import ( "github.com/xssnick/tonutils-go/ton" "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" ) @@ -71,6 +72,21 @@ type ParserService interface { others func(context.Context, addr.Address) (*core.AccountState, error), ) error + ParseAccountContractData( + ctx context.Context, + contractDesc *core.ContractInterface, + acc *core.AccountState, + others func(context.Context, addr.Address) (*core.AccountState, error), + ) error + + ExecuteAccountGetMethod( + ctx context.Context, + contract abi.ContractName, + getMethod string, + acc *core.AccountState, + others func(context.Context, addr.Address) (*core.AccountState, error), + ) error + ParseMessagePayload( ctx context.Context, message *core.Message, // source and destination account states must be known diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index ef0700e9..27905cea 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -63,6 +63,23 @@ func matchByGetMethods(acc *core.AccountState, getMethodHashes []int32) bool { return true } +func interfaceMatched(acc *core.AccountState, i *core.ContractInterface) bool { + if matchByAddress(acc, i.Addresses) { + return true + } + + if matchByCode(acc, i.Code) { + return true + } + + if len(i.Addresses) == 0 && len(i.Code) == 0 && matchByGetMethods(acc, i.GetMethodHashes) { + // match by get methods only if code and addresses are not set + return true + } + + return false +} + func (s *Service) determineInterfaces(ctx context.Context, acc *core.AccountState) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface @@ -72,22 +89,8 @@ func (s *Service) determineInterfaces(ctx context.Context, acc *core.AccountStat } for _, i := range interfaces { - if matchByAddress(acc, i.Addresses) { + if interfaceMatched(acc, i) { ret = append(ret, i) - continue - } - - if matchByCode(acc, i.Code) { - ret = append(ret, i) - continue - } - - if len(i.Addresses) != 0 || len(i.Code) != 0 { - continue // match by get methods only if code and addresses are not set - } - if matchByGetMethods(acc, i.GetMethodHashes) { - ret = append(ret, i) - continue } } @@ -115,11 +118,95 @@ func (s *Service) ParseAccountData( for _, i := range interfaces { acc.Types = append(acc.Types, i.Name) } + acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} + + s.callPossibleGetMethods(ctx, acc, others, interfaces) + + return nil +} + +func (s *Service) ParseAccountContractData( + ctx context.Context, + contractDesc *core.ContractInterface, + acc *core.AccountState, + others func(context.Context, addr.Address) (*core.AccountState, error), +) error { + if !interfaceMatched(acc, contractDesc) { + return errors.Wrap(core.ErrInvalidArg, "account state does not match the contract interface description") + } + + var contractTypeSet bool + for _, t := range acc.Types { + if t == contractDesc.Name { + contractTypeSet = true + break + } + } + if !contractTypeSet { + acc.Types = append(acc.Types, contractDesc.Name) + } + if acc.ExecutedGetMethods == nil { acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} } + if _, ok := acc.ExecutedGetMethods[contractDesc.Name]; ok { + delete(acc.ExecutedGetMethods, contractDesc.Name) + } - s.callPossibleGetMethods(ctx, acc, others, interfaces) + s.callPossibleGetMethods(ctx, acc, others, []*core.ContractInterface{contractDesc}) return nil } + +func (s *Service) ExecuteAccountGetMethod( + ctx context.Context, + contract abi.ContractName, + getMethod string, + acc *core.AccountState, + others func(context.Context, addr.Address) (*core.AccountState, error), +) error { + if s.ContractRepo == nil { + return errors.Wrap(app.ErrImpossibleParsing, "no contract repository") + } + + interfaces, err := s.determineInterfaces(ctx, acc) + if err != nil { + return errors.Wrapf(err, "determine contract interfaces") + } + if len(interfaces) == 0 { + return errors.Wrap(app.ErrImpossibleParsing, "unknown contract interfaces") + } + + var ( + i *core.ContractInterface + d *abi.GetMethodDesc + ) + for _, i = range interfaces { + if i.Name == contract { + break + } + } + if i == nil { + return errors.Wrapf(core.ErrInvalidArg, + "cannot find '%s' interface description for '%s' account", contract, acc.Address) + } + for _, d = range i.GetMethodsDesc { + if d.Name == getMethod { + break + } + } + if d == nil { + return errors.Wrapf(core.ErrInvalidArg, + "cannot find '%s' get-method description for '%s' account and '%s' interface", getMethod, acc.Address, contract) + } + + acc.Types = nil + for _, i := range interfaces { + acc.Types = append(acc.Types, i.Name) + } + if acc.ExecutedGetMethods == nil { + acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} + } + + return s.callGetMethod(ctx, acc, i, d, others) +} From 8a61b1ba6aba24c2aba6c7da3d58e975e80013e1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 23:21:25 +0700 Subject: [PATCH 109/186] [rescan] refactor task to partially read database --- internal/app/rescan.go | 2 + internal/app/rescan/account.go | 262 +++++++++++++++++++++++++++------ internal/app/rescan/rescan.go | 123 ++++++++-------- internal/app/rescan/tx.go | 143 ++++++++++-------- 4 files changed, 365 insertions(+), 165 deletions(-) diff --git a/internal/app/rescan.go b/internal/app/rescan.go index 2fe1c811..782fbc6e 100644 --- a/internal/app/rescan.go +++ b/internal/app/rescan.go @@ -14,6 +14,8 @@ type RescanConfig struct { Parser ParserService Workers int + + SelectLimit int } type RescanService interface { diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 0ba52159..270487ac 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -1,6 +1,7 @@ package rescan import ( + "bytes" "context" "reflect" "sync" @@ -9,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/abi/known" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" @@ -16,15 +18,13 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (s *Service) getRecentAccountState(ctx context.Context, master, b core.BlockID, a addr.Address) (*core.AccountState, error) { +func (s *Service) getRecentAccountState(ctx context.Context, b core.BlockID, a addr.Address) (*core.AccountState, error) { defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) var boundBlock core.BlockID switch { case b.Workchain == int32(a.Workchain()): boundBlock = b - case master.Workchain == int32(a.Workchain()): - boundBlock = master default: return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) } @@ -48,78 +48,252 @@ func (s *Service) getRecentAccountState(ctx context.Context, master, b core.Bloc return accountRes.Rows[0], nil } -func (s *Service) rescanAccountsInBlock(master, b *core.Block) (updates []*core.AccountState) { - for _, acc := range b.Accounts { - if known.IsOnlyWalletInterfaces(acc.Types) { - // we do not want to emulate wallet get-methods once again, - // as there are lots of them, so it takes a lot of CPU usage +func copyAccountState(state *core.AccountState) *core.AccountState { + update := *state + update.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} + for n, e := range state.ExecutedGetMethods { + update.ExecutedGetMethods[n] = make([]abi.GetMethodExecution, len(e)) + copy(update.ExecutedGetMethods[n], e) + } + return &update +} + +func (s *Service) clearParsedAccountsData(task *core.RescanTask, acc *core.AccountState) { + _, ok := acc.ExecutedGetMethods[task.ContractName] + if !ok { + return + } + + delete(acc.ExecutedGetMethods, task.ContractName) + + switch task.ContractName { + case known.NFTCollection, known.NFTItem, known.JettonMinter, known.JettonWallet: + acc.MinterAddress = nil + acc.OwnerAddress = nil + + acc.ContentURI = "" + acc.ContentName = "" + acc.ContentDescription = "" + acc.ContentImage = "" + acc.ContentImageData = nil + + acc.Fake = false + + acc.JettonBalance = nil + } +} + +func (s *Service) parseAccountData(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { + if known.IsOnlyWalletInterfaces(acc.Types) { + // we do not want to emulate wallet get-methods once again, + // as there are lots of them, so it takes a lot of CPU usage + return + } + + getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + return s.getRecentAccountState(ctx, acc.BlockID(), a) + } + + err := s.Parser.ParseAccountContractData(ctx, task.Contract, acc, getOtherAccountFunc) + if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + log.Error().Err(err).Str("addr", acc.Address.Base64()).Msg("parse account data") + } +} + +func (s *Service) rescanInterface(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { + s.clearParsedAccountsData(task, acc) + + if task.Type == core.DelInterface { + return + } + + s.parseAccountData(ctx, task, acc) +} + +func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.AccountState) { + _, ok := acc.ExecutedGetMethods[task.ContractName] + if !ok { + return + } + + for it, exec := range acc.ExecutedGetMethods[task.ContractName] { + if exec.Name != task.ChangedGetMethod { continue } + executions := acc.ExecutedGetMethods[task.ContractName] + copy(executions[it:], executions[it+1:]) + acc.ExecutedGetMethods[task.ContractName] = executions[:len(executions)-1] + break + } + + switch task.ContractName { + case known.NFTCollection, known.NFTItem, known.JettonMinter, known.JettonWallet: + default: + return + } + + switch task.ChangedGetMethod { + case "get_nft_content", "get_collection_data", "get_jetton_data": + acc.ContentURI = "" + acc.ContentName = "" + acc.ContentDescription = "" + acc.ContentImage = "" + acc.ContentImageData = nil + } + + switch task.ChangedGetMethod { + case "get_collection_data": + acc.OwnerAddress = nil + case "get_nft_data", "get_wallet_data": + acc.OwnerAddress = nil + acc.MinterAddress = nil + acc.Fake = false + + acc.JettonBalance = nil + } - getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - return s.getRecentAccountState(ctx, master.ID(), b.ID(), a) + switch task.ChangedGetMethod { + case "get_wallet_address", "get_nft_address_by_index": + acc.Fake = false + } +} + +func (s *Service) executeGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { + getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { + return s.getRecentAccountState(ctx, acc.BlockID(), a) + } + + err := s.Parser.ExecuteAccountGetMethod(ctx, task.ContractName, task.ChangedGetMethod, acc, getOtherAccountFunc) + if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + log.Error().Err(err). + Str("contract_name", string(task.ContractName)). + Str("get_method", task.ChangedGetMethod). + Str("addr", acc.Address.Base64()). + Msg("parse account data") + } +} + +func (s *Service) rescanGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { + s.clearExecutedGetMethod(task, acc) + + matchedByGetMethod := func() (matchedByGM, hasGM bool) { + if len(task.Contract.Code) > 0 || len(task.Contract.Addresses) > 0 { + return false, false } - update := *acc + changed := abi.MethodNameHash(task.ChangedGetMethod) + for _, gmh := range task.Contract.GetMethodHashes { + if gmh == changed { + return true, true + } + } + return true, false + } - err := s.Parser.ParseAccountData(context.Background(), &update, getOtherAccountFunc) - if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { - log.Error().Err(err).Str("addr", update.Address.Base64()).Msg("parse account data") - continue + switch task.Type { + case core.AddGetMethod: + m, h := matchedByGetMethod() + if m && !h { + // clear all parsed data in account states lacking the new get method + s.clearParsedAccountsData(task, acc) + return } - if reflect.DeepEqual(acc, &update) { - continue + s.executeGetMethod(ctx, task, acc) + + case core.DelGetMethod: + m, h := matchedByGetMethod() + if m && !h { + // include all account states that match the contract interface description, + // minus the deleted get method + s.parseAccountData(ctx, task, acc) + return } - updates = append(updates, &update) + + case core.UpdGetMethod: + s.executeGetMethod(ctx, task, acc) } - return updates } -func (s *Service) rescanAccountsWorker(b *core.Block) (updates []*core.AccountState) { - for _, shard := range b.Shards { - upd := s.rescanAccountsInBlock(b, shard) - updates = append(updates, upd...) - } +func (s *Service) rescanAccountsWorker(ctx context.Context, task *core.RescanTask, batch []*core.AccountState) (updates []*core.AccountState) { + for _, acc := range batch { + update := copyAccountState(acc) - upd := s.rescanAccountsInBlock(b, b) - updates = append(updates, upd...) + switch task.Type { + case core.AddInterface, core.UpdInterface, core.DelInterface: + s.rescanInterface(ctx, task, update) + case core.AddGetMethod, core.UpdGetMethod, core.DelGetMethod: + s.rescanGetMethod(ctx, task, update) + } + + if reflect.DeepEqual(acc, &update) { + continue + } + + updates = append(updates, update) + } return updates } -func (s *Service) rescanAccounts(masterBlocks []*core.Block) (lastScanned uint32) { +func (s *Service) rescanAccounts(ctx context.Context, task *core.RescanTask, ids []*core.AccountStateID) error { var ( - accountUpdates = make(chan []*core.AccountState, len(masterBlocks)) - scanWG sync.WaitGroup + lastScanned core.AccountStateID + updatesAll []*core.AccountState + updatesChan = make(chan []*core.AccountState) + scanWG sync.WaitGroup ) - scanWG.Add(len(masterBlocks)) + accRet, err := s.AccountRepo.FilterAccounts(ctx, &filter.AccountsReq{StateIDs: ids}) + if err != nil { + return errors.Wrapf(err, "filter accounts") + } + accounts := accRet.Rows - for _, b := range masterBlocks { - go func(master *core.Block) { - defer scanWG.Done() - accountUpdates <- s.rescanAccountsWorker(master) - }(b) + workers := s.Workers + if len(accounts) < workers { + workers = len(accounts) + } - if b.SeqNo > lastScanned { - lastScanned = b.SeqNo + for i := 0; i < len(accounts); { + batchLen := (len(accounts) - i) / workers + if (len(accounts)-i)%workers != 0 { + batchLen++ } + + scanWG.Add(1) + go func(batch []*core.AccountState) { + defer scanWG.Done() + updatesChan <- s.rescanAccountsWorker(ctx, task, batch) + }(accounts[i : i+batchLen]) + + i += batchLen } go func() { scanWG.Wait() - close(accountUpdates) + close(updatesChan) }() - var allUpdates []*core.AccountState - for upd := range accountUpdates { - allUpdates = append(allUpdates, upd...) + for upd := range updatesChan { + for _, upd := range upd { + updID := core.AccountStateID{ + Address: upd.Address, + LastTxLT: upd.LastTxLT, + } + if bytes.Compare(lastScanned.Address[:], updID.Address[:]) >= 0 && lastScanned.LastTxLT >= updID.LastTxLT { + lastScanned = updID + } + } + updatesAll = append(updatesAll, upd...) } - if err := s.AccountRepo.UpdateAccountStates(context.Background(), allUpdates); err != nil { - return 0 + if err := s.AccountRepo.UpdateAccountStates(ctx, updatesAll); err != nil { + return errors.Wrapf(err, "update account states") } - return lastScanned + task.LastAddress = &lastScanned.Address + task.LastTxLt = lastScanned.LastTxLT + + return nil } diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index bfcec078..e298701a 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -7,10 +7,10 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" - "github.com/tonindexer/anton/internal/core/filter" ) var _ app.RescanService = (*Service)(nil) @@ -18,8 +18,6 @@ var _ app.RescanService = (*Service)(nil) type Service struct { *app.RescanConfig - masterShard int64 - interfacesCache *interfacesCache run bool @@ -75,14 +73,6 @@ func (s *Service) Stop() { func (s *Service) rescanLoop() { defer s.wg.Done() - lastMaster, err := s.BlockRepo.GetLastMasterBlock(context.Background()) - if err != nil { - log.Error().Err(err).Msg("cannot get last masterchain block") - return - } - toBlock := lastMaster.SeqNo - s.masterShard = lastMaster.Shard - for s.running() { tx, task, err := s.ContractRepo.GetUnfinishedRescanTask(context.Background()) if err != nil { @@ -93,7 +83,7 @@ func (s *Service) rescanLoop() { continue } - if err := s.rescanRunTask(task, toBlock); err != nil { + if err := s.rescanRunTask(context.Background(), task); err != nil { _ = tx.Rollback() log.Error().Err(err). Int("id", task.ID). @@ -110,71 +100,80 @@ func (s *Service) rescanLoop() { } } -func (s *Service) rescanRunTask(task *core.RescanTask, toBlock uint32) error { - if task.AccountsRescanDone || task.AccountsLastMaster >= toBlock { - task.AccountsRescanDone = true - } else { - if task.AccountsLastMaster == 0 { - task.AccountsLastMaster = task.StartFrom - 1 +func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { + switch task.Type { + case core.AddInterface: + var codeHash []byte + if task.Contract.Code != nil { + codeCell, err := cell.FromBOC(task.Contract.Code) + if err != nil { + return errors.Wrapf(err, "making %s code cell from boc", task.Contract.Name) + } + codeHash = codeCell.Hash() } - blocks, err := s.filterBlocksForRescan(task.AccountsLastMaster+1, toBlock, true, false) + + ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, "", task.Contract.Addresses, codeHash, task.Contract.GetMethodHashes, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { - return errors.Wrap(err, "filter blocks for account states rescan") + return errors.Wrapf(err, "match states by interface description") } - if lastScanned := s.rescanAccounts(blocks); lastScanned != 0 { - task.AccountsLastMaster = lastScanned + if len(ids) == 0 { + task.Finished = true + return nil } - } - if task.MessagesRescanDone || task.MessagesLastMaster >= toBlock { - task.MessagesRescanDone = true - } else if task.AccountsRescanDone { - if task.MessagesLastMaster == 0 { - task.MessagesLastMaster = task.StartFrom - 1 + if err := s.rescanAccounts(ctx, task, ids); err != nil { + return errors.Wrapf(err, "rescan accounts") } - blocks, err := s.filterBlocksForRescan(task.MessagesLastMaster+1, toBlock, false, true) + + return nil + + case core.UpdInterface, core.DelInterface: + ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, task.ContractName, nil, nil, nil, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { - return errors.Wrap(err, "filter blocks for messages states rescan") + return errors.Wrapf(err, "match states by interface description") } - if lastScanned := s.rescanMessages(blocks); lastScanned != 0 { - task.MessagesLastMaster = lastScanned + if len(ids) == 0 { + task.Finished = true + return nil } - } - if task.AccountsRescanDone && task.MessagesRescanDone { - task.Finished = true - } + if err := s.rescanAccounts(ctx, task, ids); err != nil { + return errors.Wrapf(err, "rescan accounts") + } - return nil -} + return nil -func (s *Service) filterBlocksForRescan(fromBlock, toBlock uint32, withAccounts, withMessages bool) ([]*core.Block, error) { - workers := s.Workers - if delta := int(toBlock-fromBlock) + 1; delta < workers { - workers = delta - } + case core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod: + var codeHash []byte + if task.Contract.Code != nil { + codeCell, err := cell.FromBOC(task.Contract.Code) + if err != nil { + return errors.Wrapf(err, "making %s code cell from boc", task.Contract.Name) + } + codeHash = codeCell.Hash() + } - req := &filter.BlocksReq{ - Workchain: new(int32), - Shard: new(int64), - WithShards: true, - WithAccountStates: withAccounts, - WithTransactions: withMessages, - WithTransactionMessages: withMessages, - AfterSeqNo: new(uint32), - Order: "ASC", - Limit: workers, - } - *req.Workchain = -1 - *req.Shard = s.masterShard - *req.AfterSeqNo = fromBlock - 1 + ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, task.ContractName, task.Contract.Addresses, codeHash, task.Contract.GetMethodHashes, task.LastAddress, task.LastTxLt, s.SelectLimit) + if err != nil { + return errors.Wrapf(err, "match states by interface description") + } - defer app.TimeTrack(time.Now(), "filterBlocksForRescan(%d, %d, %t)", fromBlock-1, workers, withMessages) + if err := s.rescanAccounts(ctx, task, ids); err != nil { + return errors.Wrapf(err, "rescan accounts") + } + + return nil + + case core.DelOperation, core.UpdOperation: + hashes, err := s.MessageRepo.MatchMessagesByOperationDesc(ctx, task.ContractName, task.MessageType, task.Outgoing, task.OperationID, task.LastAddress, task.LastTxLt, s.SelectLimit) + if err != nil { + return errors.Wrapf(err, "get addresses by contract name") + } - res, err := s.BlockRepo.FilterBlocks(context.Background(), req) - if err != nil { - return nil, err + if err := s.rescanMessages(ctx, task, hashes); err != nil { + return errors.Wrapf(err, "rescan messages") + } } - return res.Rows, nil + return errors.Wrapf(core.ErrInvalidArg, "unknown rescan task type %s", task.Type) } diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 968a23bf..64f7d3b3 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -1,6 +1,7 @@ package rescan import ( + "bytes" "context" "reflect" "sync" @@ -57,103 +58,127 @@ func (s *Service) getAccountStateForMessage(ctx context.Context, a addr.Address, return &core.AccountState{Address: a, Types: s.chooseInterfaces(interfaceUpdates, txLT)} } -func (s *Service) rescanMessage(ctx context.Context, msg *core.Message) *core.Message { +func (s *Service) rescanMessage(ctx context.Context, task *core.RescanTask, update *core.Message) error { // we must get account state's interfaces to properly determine message operation // and to parse message accordingly // so for the source of the message we take the account state of the sender, - // which was updated just before the message was sent + // which is updated just before the message is sent // for the destination of the given message, we take the account state of receiver, - // which was update just after the message was received - - msg.SrcState = s.getAccountStateForMessage(ctx, msg.SrcAddress, msg.SrcTxLT) - msg.DstState = s.getAccountStateForMessage(ctx, msg.DstAddress, msg.DstTxLT) - - update := *msg + // which is updated just after the message is received + + if task.Outgoing { + update.SrcState = &core.AccountState{Address: update.SrcAddress, Types: []abi.ContractName{task.ContractName}} + update.DstState = s.getAccountStateForMessage(ctx, update.DstAddress, update.DstTxLT) + } else { + update.SrcState = s.getAccountStateForMessage(ctx, update.SrcAddress, update.SrcTxLT) + update.DstState = &core.AccountState{Address: update.DstAddress, Types: []abi.ContractName{task.ContractName}} + } - err := s.Parser.ParseMessagePayload(ctx, &update) + err := s.Parser.ParseMessagePayload(ctx, update) if err != nil { if !errors.Is(err, app.ErrImpossibleParsing) { log.Error().Err(err). - Hex("msg_hash", msg.Hash). - Hex("src_tx_hash", msg.SrcTxHash). - Str("src_addr", msg.SrcAddress.String()). - Hex("dst_tx_hash", msg.DstTxHash). - Str("dst_addr", msg.DstAddress.String()). - Uint32("op_id", msg.OperationID). + Hex("msg_hash", update.Hash). + Hex("src_tx_hash", update.SrcTxHash). + Str("src_addr", update.SrcAddress.String()). + Hex("dst_tx_hash", update.DstTxHash). + Str("dst_addr", update.DstAddress.String()). + Uint32("op_id", update.OperationID). Msg("parse message payload") } - return nil - } - - if reflect.DeepEqual(msg, &update) { - return nil + return err } - return &update + return nil } -func (s *Service) rescanMessagesInBlock(ctx context.Context, b *core.Block) (updates []*core.Message) { - for _, tx := range b.Transactions { - if tx.InMsg != nil { - if got := s.rescanMessage(ctx, tx.InMsg); got != nil { - updates = append(updates, got) - } - } - for _, out := range tx.OutMsg { - if got := s.rescanMessage(ctx, out); got != nil { - updates = append(updates, got) +func (s *Service) rescanMessagesWorker(ctx context.Context, task *core.RescanTask, messages []*core.Message) (updates []*core.Message) { + for _, msg := range messages { + upd := *msg + + switch task.Type { + case core.DelOperation: + upd.SrcContract, upd.DstContract, upd.OperationName, upd.DataJSON, upd.Error = "", "", "", nil, "" + + case core.UpdOperation: + if err := s.rescanMessage(ctx, task, msg); err != nil { + continue } } - } - return updates -} -func (s *Service) rescanMessagesWorker(m *core.Block) (updates []*core.Message) { - for _, shard := range m.Shards { - upd := s.rescanMessagesInBlock(context.Background(), shard) - updates = append(updates, upd...) + if !reflect.DeepEqual(msg, &upd) { + updates = append(updates, &upd) + } } - upd := s.rescanMessagesInBlock(context.Background(), m) - updates = append(updates, upd...) - return updates } -func (s *Service) rescanMessages(masterBlocks []*core.Block) (lastScanned uint32) { +func (s *Service) rescanMessages(ctx context.Context, task *core.RescanTask, hashes [][]byte) error { var ( - msgUpdates = make(chan []*core.Message, len(masterBlocks)) - scanWG sync.WaitGroup + lastParsed core.AccountStateID + updatesAll []*core.Message + updatesChan = make(chan []*core.Message) + scanWG sync.WaitGroup ) - scanWG.Add(len(masterBlocks)) + messages, err := s.MessageRepo.GetMessages(ctx, hashes) + if err != nil { + return err + } - for _, b := range masterBlocks { - go func(master *core.Block) { - defer scanWG.Done() - msgUpdates <- s.rescanMessagesWorker(master) - }(b) + workers := s.Workers + if len(messages) < workers { + workers = len(messages) + } - if b.SeqNo > lastScanned { - lastScanned = b.SeqNo + for i := 0; i < len(messages); { + batchLen := (len(messages) - i) / workers + if (len(messages)-i)%workers != 0 { + batchLen++ } + + scanWG.Add(1) + go func(batch []*core.Message) { + defer scanWG.Done() + updatesChan <- s.rescanMessagesWorker(ctx, task, batch) + }(messages[i : i+batchLen]) + + i += batchLen } go func() { scanWG.Wait() - close(msgUpdates) + close(updatesChan) }() - var allUpdates []*core.Message - for upd := range msgUpdates { - allUpdates = append(allUpdates, upd...) + for upd := range updatesChan { + for _, upd := range upd { + updID := core.AccountStateID{ + Address: upd.DstAddress, + LastTxLT: upd.DstTxLT, + } + if task.Outgoing { + updID = core.AccountStateID{ + Address: upd.SrcAddress, + LastTxLT: upd.SrcTxLT, + } + } + if bytes.Compare(lastParsed.Address[:], updID.Address[:]) >= 0 && lastParsed.LastTxLT >= updID.LastTxLT { + lastParsed = updID + } + } + updatesAll = append(updatesAll, upd...) } - if err := s.MessageRepo.UpdateMessages(context.Background(), allUpdates); err != nil { - return 0 + if err := s.MessageRepo.UpdateMessages(context.Background(), updatesAll); err != nil { + return errors.Wrap(err, "update messages") } - return lastScanned + task.LastAddress = &lastParsed.Address + task.LastTxLt = lastParsed.LastTxLT + + return nil } From d00cfdb891547fb9df210cc94fd9b9f9f8c5f09c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 23:29:19 +0700 Subject: [PATCH 110/186] [repo] account: fix ch hasAny select query --- internal/core/repository/account/filter.go | 6 +++--- internal/core/repository/account/filter_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/core/repository/account/filter.go b/internal/core/repository/account/filter.go index da39cad7..285a54df 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -45,7 +45,7 @@ func (r *Repository) countAddressLabels(ctx context.Context, f *filter.LabelsReq q = q.Where("positionCaseInsensitive(name, ?) > 0", f.Name) } if len(f.Categories) > 0 { - q = q.Where("hasAny(categories, [?])", ch.In(f.Categories)) + q = q.Where("hasAny(categories, ?)", ch.Array(f.Categories)) } return q.Count(ctx) @@ -187,7 +187,7 @@ func (r *Repository) countAccountStates(ctx context.Context, f *filter.AccountsR } if len(f.ContractTypes) > 0 { - q = q.Where("hasAny(types, [?])", ch.In(f.ContractTypes)) + q = q.Where("hasAny(types, ?)", ch.Array(f.ContractTypes)) } if f.MinterAddress != nil { q = q.Where("minter_address = ?", f.MinterAddress) @@ -225,7 +225,7 @@ func (r *Repository) FilterAccounts(ctx context.Context, f *filter.AccountsReq) res.Total, err = r.countAccountStates(ctx, f) if err != nil { - return res, err + return res, errors.Wrap(err, "count account states") } if res.Total == 0 { return res, nil diff --git a/internal/core/repository/account/filter_test.go b/internal/core/repository/account/filter_test.go index 82e4a02a..96cebbd8 100644 --- a/internal/core/repository/account/filter_test.go +++ b/internal/core/repository/account/filter_test.go @@ -218,7 +218,7 @@ 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{ - ContractTypes: []abi.ContractName{"special"}, + ContractTypes: []abi.ContractName{"special", "some_nonsense"}, LatestState: true, Order: "DESC", Limit: 1, }) From 63526bddb41956fe21889e818ffc3dc31d1d74f1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 11 Mar 2024 23:38:27 +0700 Subject: [PATCH 111/186] [repo] account: fix other ch hasAny select queries --- internal/core/repository/account/account.go | 2 +- internal/core/repository/account/history.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 4303372a..c2ded93e 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -309,7 +309,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, } if len(addresses) == 0 && len(codeHash) == 0 && len(getMethodHashes) > 0 { // match by get-method hashes only if addresses and code_hash are not set - q = q.WhereOr("hasAll(get_method_hashes, [?])", ch.In(getMethodHashes)) + q = q.WhereOr("hasAll(get_method_hashes, ?)", ch.Array(getMethodHashes)) } return q }) diff --git a/internal/core/repository/account/history.go b/internal/core/repository/account/history.go index 213b0885..ed0e91ff 100644 --- a/internal/core/repository/account/history.go +++ b/internal/core/repository/account/history.go @@ -29,7 +29,7 @@ func (r *Repository) AggregateAccountsHistory(ctx context.Context, req *history. q := r.ch.NewSelect().Model((*core.AccountState)(nil)) if len(req.ContractTypes) > 0 { - q = q.Where("hasAny(types, [?])", ch.In(getContractTypes(req.ContractTypes))) + q = q.Where("hasAny(types, ?)", ch.Array(getContractTypes(req.ContractTypes))) } if req.MinterAddress != nil { q = q.Where("minter_address = ?", req.MinterAddress) From e0d4f948ccdaccc12c4009a09a577016f1c1a7d8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 17:04:11 +0700 Subject: [PATCH 112/186] [abi] get-emulator: add args and return values description for execution --- abi/get_emulator.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/abi/get_emulator.go b/abi/get_emulator.go index 5709fcd1..e80fff80 100644 --- a/abi/get_emulator.go +++ b/abi/get_emulator.go @@ -37,8 +37,11 @@ type GetMethodExecution struct { Address *addr.Address `json:"address,omitempty"` - Receives []any `json:"receives,omitempty"` - Returns []any `json:"returns,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"` } From 5a27faf96a9fb5adca07e161953bc505a1013c63 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 17:04:31 +0700 Subject: [PATCH 113/186] [rescan] clearExecutedGetMethod: fix lint --- internal/app/rescan/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 270487ac..51493a1f 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -116,8 +116,8 @@ func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.Accoun return } - for it, exec := range acc.ExecutedGetMethods[task.ContractName] { - if exec.Name != task.ChangedGetMethod { + for it := range acc.ExecutedGetMethods[task.ContractName] { + if acc.ExecutedGetMethods[task.ContractName][it].Name != task.ChangedGetMethod { continue } executions := acc.ExecutedGetMethods[task.ContractName] From 310760a199d2c0131b420312fcd543d506833998 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 17:09:35 +0700 Subject: [PATCH 114/186] [repo] contract: add methods for update and deletion of descriptions --- internal/core/contract.go | 8 +- internal/core/repository/contract/contract.go | 100 +++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/internal/core/contract.go b/internal/core/contract.go index ffbb55d5..7e4d2d06 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -104,14 +104,20 @@ type RescanTask struct { type ContractRepository interface { AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error + UpdateDefinition(ctx context.Context, dn abi.TLBType, d abi.TLBFieldsDesc) error + DeleteDefinition(ctx context.Context, dn abi.TLBType) error GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) AddInterface(context.Context, *ContractInterface) error - DelInterface(ctx context.Context, name string) error + UpdateInterface(ctx context.Context, i *ContractInterface) error + DeleteInterface(ctx context.Context, name abi.ContractName) error + GetInterface(ctx context.Context, name abi.ContractName) (*ContractInterface, error) GetInterfaces(context.Context) ([]*ContractInterface, error) GetMethodDescription(ctx context.Context, name abi.ContractName, method string) (abi.GetMethodDesc, error) AddOperation(context.Context, *ContractOperation) error + UpdateOperation(ctx context.Context, op *ContractOperation) error + DeleteOperation(ctx context.Context, opName string) error GetOperations(context.Context) ([]*ContractOperation, error) GetOperationByID(context.Context, MessageType, []abi.ContractName, bool, uint32) (*ContractOperation, error) diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index a09a4636..013052a6 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -109,6 +109,47 @@ func (r *Repository) AddDefinition(ctx context.Context, dn abi.TLBType, d abi.TL return nil } +func (r *Repository) UpdateDefinition(ctx context.Context, dn abi.TLBType, d abi.TLBFieldsDesc) error { + def := &core.ContractDefinition{ + Name: dn, + Schema: d, + } + + ret, err := r.pg.NewUpdate().Model(def).WherePK().Exec(ctx) + if err != nil { + return err + } + + rows, err := ret.RowsAffected() + if err != nil { + return errors.Wrap(err, "rows affected") + } + if rows == 0 { + return core.ErrNotFound + } + + return nil +} + +func (r *Repository) DeleteDefinition(ctx context.Context, dn abi.TLBType) error { + def := &core.ContractDefinition{Name: dn} + + ret, err := r.pg.NewUpdate().Model(def).WherePK().Exec(ctx) + if err != nil { + return err + } + + rows, err := ret.RowsAffected() + if err != nil { + return errors.Wrap(err, "rows affected") + } + if rows == 0 { + return core.ErrNotFound + } + + return nil +} + func (r *Repository) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { var ret []*core.ContractDefinition @@ -133,7 +174,15 @@ func (r *Repository) AddInterface(ctx context.Context, i *core.ContractInterface return nil } -func (r *Repository) DelInterface(ctx context.Context, name string) error { +func (r *Repository) UpdateInterface(ctx context.Context, i *core.ContractInterface) error { + _, err := r.pg.NewUpdate().Model(i).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} + +func (r *Repository) DeleteInterface(ctx context.Context, name abi.ContractName) error { _, err := r.pg.NewDelete(). Model((*core.ContractOperation)(nil)). Where("contract_name = ?", name). @@ -161,6 +210,23 @@ func (r *Repository) DelInterface(ctx context.Context, name string) error { return nil } +func (r *Repository) GetInterface(ctx context.Context, name abi.ContractName) (*core.ContractInterface, error) { + var ret core.ContractInterface + + err := r.pg.NewSelect().Model(&ret). + Relation("Operations"). + Where("name = ?", name). + Scan(ctx) + if errors.Is(err, sql.ErrNoRows) { + return nil, core.ErrNotFound + } + if err != nil { + return nil, err + } + + return &ret, nil +} + func (r *Repository) GetInterfaces(ctx context.Context) ([]*core.ContractInterface, error) { var ret []*core.ContractInterface @@ -209,6 +275,38 @@ func (r *Repository) AddOperation(ctx context.Context, op *core.ContractOperatio return nil } +func (r *Repository) UpdateOperation(ctx context.Context, op *core.ContractOperation) error { + ret, err := r.pg.NewUpdate().Model(op).WherePK().Exec(ctx) + if err != nil { + return err + } + rows, err := ret.RowsAffected() + if err != nil { + return errors.Wrap(err, "rows affected") + } + if rows == 0 { + return errors.Wrapf(core.ErrNotFound, "no operation '%s'", op.OperationName) + } + return nil +} + +func (r *Repository) DeleteOperation(ctx context.Context, opName string) error { + ret, err := r.pg.NewDelete().Model((*core.ContractOperation)(nil)). + Where("operation_name = ?", opName). + Exec(ctx) + if err != nil { + return err + } + rows, err := ret.RowsAffected() + if err != nil { + return errors.Wrap(err, "rows affected") + } + if rows == 0 { + return errors.Wrapf(core.ErrNotFound, "no operation '%s'", opName) + } + return nil +} + func (r *Repository) GetOperations(ctx context.Context) ([]*core.ContractOperation, error) { var ret []*core.ContractOperation From 58f9a52526a795600e930470f5d6c25cd84aed9c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 17:10:41 +0700 Subject: [PATCH 115/186] [cmd] db: fix mark_applied subcommand name --- cmd/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/db/db.go b/cmd/db/db.go index 1c9b15f5..c5f00c92 100644 --- a/cmd/db/db.go +++ b/cmd/db/db.go @@ -273,7 +273,7 @@ var Command = &cli.Command{ }, }, { - Name: "mark_applied", + Name: "markApplied", Usage: "Marks migrations as applied without actually running them", Action: func(c *cli.Context) error { mpg, mch, err := newMigrators() From 637a02818ee84ef82d26d5ac952e783c4fb0c85c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 19:09:24 +0700 Subject: [PATCH 116/186] [repo] create rescan repository --- internal/core/contract.go | 67 ------------ internal/core/repository/contract/contract.go | 76 ------------- internal/core/repository/rescan/rescan.go | 101 ++++++++++++++++++ internal/core/rescan.go | 80 ++++++++++++++ 4 files changed, 181 insertions(+), 143 deletions(-) create mode 100644 internal/core/repository/rescan/rescan.go create mode 100644 internal/core/rescan.go diff --git a/internal/core/contract.go b/internal/core/contract.go index 7e4d2d06..fec9bc7f 100644 --- a/internal/core/contract.go +++ b/internal/core/contract.go @@ -38,70 +38,6 @@ type ContractOperation struct { Schema abi.OperationDesc `bun:"type:jsonb" json:"schema"` } -type RescanTaskType string - -const ( - // AddInterface task filters all account states by suitable addresses, code, or get method hashes. - // From these account states, extract (address, last_tx_lt) pairs, - // execute get methods on these pairs, and update the account states with the newly parsed data. - AddInterface RescanTaskType = "add_interface" - - // UpdInterface filters the account states by the already set contract name. - // Again, collect (address, last_tx_lt) pairs, execute get methods, - // update the account states with the parsed data. - UpdInterface RescanTaskType = "upd_interface" - - // DelInterface does the same filtering as UpdInterface, - // but it clears any previously parsed data. - DelInterface RescanTaskType = "del_interface" - - // AddGetMethod task executes this method across all account states that were previously scanned - // and clears all parsed data in account states lacking the new get method. - AddGetMethod RescanTaskType = "add_get_method" - - // DelGetMethod task eliminates the execution of this get-method in all previously parsed account states. - // Then, it includes all account states that match the contract interface description, minus the deleted get method. - DelGetMethod RescanTaskType = "del_get_method" - - // UpdGetMethod task simply iterates through all parsed account states associated with the specified contract name - // and re-execute the changed get method. - UpdGetMethod RescanTaskType = "upd_get_method" - - // UpdOperation task parses contract messages. - // It iterates through all messages with specified operation id, - // directed to (or originating from, in the case of outgoing operations) the given contract - // and adds the parsed data. - UpdOperation RescanTaskType = "upd_operation" - - // DelOperation task is the same algorithm, as UpdOperation, but it removes the parsed data. - DelOperation RescanTaskType = "del_operation" -) - -type RescanTask struct { - bun.BaseModel `bun:"table:rescan_tasks" json:"-"` - - ID int `bun:",pk,autoincrement"` - Finished bool `bun:"finished,notnull"` - Type RescanTaskType `bun:"type:rescan_task_type,notnull"` - - // contract being rescanned - ContractName abi.ContractName `bun:",notnull" json:"contract_name"` - Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` - - // for get-method update - ChangedGetMethod string `json:"changed_get_methods,omitempty"` - - // for operations - MessageType MessageType `json:"message_type,omitempty"` - Outgoing bool `json:"outgoing,omitempty"` // if operation is going from contract - OperationID uint32 `json:"operation_id,omitempty"` - Operation *ContractOperation `bun:"rel:has-one,join:contract_name=contract_name,join:outgoing=outgoing,join:operation_id=operation_id" json:"contract_operation"` - - // checkpoint - LastAddress *addr.Address `bun:"type:bytea" json:"last_address"` - LastTxLt uint64 `bun:"type:bigint" json:"last_tx_lt"` -} - type ContractRepository interface { AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error UpdateDefinition(ctx context.Context, dn abi.TLBType, d abi.TLBFieldsDesc) error @@ -120,7 +56,4 @@ type ContractRepository interface { DeleteOperation(ctx context.Context, opName string) error GetOperations(context.Context) ([]*ContractOperation, error) GetOperationByID(context.Context, MessageType, []abi.ContractName, bool, uint32) (*ContractOperation, error) - - GetUnfinishedRescanTask(context.Context) (bun.Tx, *RescanTask, error) - SetRescanTask(context.Context, bun.Tx, *RescanTask) error } diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 013052a6..fcb6e6d9 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -352,79 +352,3 @@ func (r *Repository) GetOperationByID(ctx context.Context, t core.MessageType, i return op, nil } - -func (r *Repository) CreateNewRescanTask(ctx context.Context, task *core.RescanTask) error { - task.ID = 0 - - _, err := r.pg.NewInsert().Model(task).Exec(ctx) - if err != nil { - if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { - return errors.Wrap(core.ErrAlreadyExists, "cannot create new task while the previous one is unfinished") - } - return err - } - - return nil -} - -func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core.RescanTask, error) { - var task core.RescanTask - - tx, err := r.pg.Begin() - if err != nil { - return bun.Tx{}, nil, err - } - - err = tx.NewSelect().Model(&task). - For("UPDATE"). - Where("finished = ?", false). - Order("id"). - Limit(1). - Scan(ctx) - if err != nil { - _ = tx.Rollback() - if errors.Is(err, sql.ErrNoRows) { - return bun.Tx{}, nil, errors.Wrap(core.ErrNotFound, "no unfinished tasks") - } - return bun.Tx{}, nil, err - } - - if task.Type != core.DelInterface { - task.Contract = new(core.ContractInterface) - err := r.pg.NewSelect().Model(task.Contract). - Where("name = ?", task.ContractName). - Scan(ctx) - if errors.Is(err, sql.ErrNoRows) { - return bun.Tx{}, nil, errors.Wrapf(core.ErrNotFound, "no %s contract interface for %s task", task.ContractName, task.Type) - } - if err != nil { - return bun.Tx{}, nil, err - } - } - if task.Type == core.UpdOperation { - task.Operation, err = r.GetOperationByID(ctx, task.MessageType, []abi.ContractName{task.ContractName}, task.Outgoing, task.OperationID) - if err != nil { - return bun.Tx{}, nil, errors.Wrapf(err, "get 0x%x operation of %s contract for %s task", task.OperationID, task.ContractName, task.Type) - } - } - - return tx, &task, nil -} - -func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.RescanTask) error { - _, err := tx.NewUpdate().Model(task). - Set("finished = ?finished"). - Set("last_address = ?last_address"). - Set("last_tx_lt = ?last_tx_lt"). - WherePK(). - Exec(ctx) - if err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - return nil -} diff --git a/internal/core/repository/rescan/rescan.go b/internal/core/repository/rescan/rescan.go new file mode 100644 index 00000000..384fa826 --- /dev/null +++ b/internal/core/repository/rescan/rescan.go @@ -0,0 +1,101 @@ +package rescan + +import ( + "context" + "database/sql" + + "github.com/pkg/errors" + "github.com/uptrace/bun" + + "github.com/tonindexer/anton/internal/core" +) + +var _ core.RescanRepository = (*Repository)(nil) + +type Repository struct { + pg *bun.DB +} + +func NewRepository(db *bun.DB) *Repository { + return &Repository{pg: db} +} + +func (r *Repository) AddRescanTask(ctx context.Context, task *core.RescanTask) error { + _, err := r.pg.NewInsert().Model(task).Exec(ctx) + if err != nil { + return err + } + return nil +} + +func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core.RescanTask, error) { + var task core.RescanTask + + tx, err := r.pg.Begin() + if err != nil { + return bun.Tx{}, nil, err + } + + err = tx.NewSelect().Model(&task). + For("UPDATE"). + Where("finished = ?", false). + Order("id"). + Limit(1). + Scan(ctx) + if err != nil { + _ = tx.Rollback() + if errors.Is(err, sql.ErrNoRows) { + return bun.Tx{}, nil, errors.Wrap(core.ErrNotFound, "no unfinished tasks") + } + return bun.Tx{}, nil, err + } + + if task.Type != core.DelInterface { + task.Contract = new(core.ContractInterface) + err := r.pg.NewSelect().Model(task.Contract). + Where("name = ?", task.ContractName). + Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = core.ErrNotFound + } + return bun.Tx{}, nil, errors.Wrapf(core.ErrNotFound, "no %s contract interface for %s task", task.ContractName, task.Type) + } + } + + if task.Type == core.UpdOperation { + task.Operation = new(core.ContractOperation) + err := r.pg.NewSelect().Model(task.Operation). + Where("contract_name = ?", task.ContractName). + Where("outgoing IS ?", task.Operation). + Where("message_type = ?", task.MessageType). + Where("operation_id = ?", task.OperationID). + Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = core.ErrNotFound + } + return bun.Tx{}, nil, errors.Wrapf(err, "get 0x%x operation of %s contract for %s task", task.OperationID, task.ContractName, task.Type) + } + } + + return tx, &task, nil +} + +func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.RescanTask) error { + _, err := tx.NewUpdate().Model(task). + Set("finished = ?finished"). + Set("last_address = ?last_address"). + Set("last_tx_lt = ?last_tx_lt"). + WherePK(). + Exec(ctx) + if err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} diff --git a/internal/core/rescan.go b/internal/core/rescan.go new file mode 100644 index 00000000..1a5ccc29 --- /dev/null +++ b/internal/core/rescan.go @@ -0,0 +1,80 @@ +package core + +import ( + "context" + + "github.com/uptrace/bun" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" +) + +type RescanTaskType string + +const ( + // AddInterface task filters all account states by suitable addresses, code, or get method hashes. + // From these account states, extract (address, last_tx_lt) pairs, + // execute get methods on these pairs, and update the account states with the newly parsed data. + AddInterface RescanTaskType = "add_interface" + + // UpdInterface filters the account states by the already set contract name. + // Again, collect (address, last_tx_lt) pairs, execute get methods, + // update the account states with the parsed data. + UpdInterface RescanTaskType = "upd_interface" + + // DelInterface does the same filtering as UpdInterface, + // but it clears any previously parsed data. + DelInterface RescanTaskType = "del_interface" + + // AddGetMethod task executes this method across all account states that were previously scanned + // and clears all parsed data in account states lacking the new get method. + AddGetMethod RescanTaskType = "add_get_method" + + // DelGetMethod task eliminates the execution of this get-method in all previously parsed account states. + // Then, it includes all account states that match the contract interface description, minus the deleted get method. + DelGetMethod RescanTaskType = "del_get_method" + + // UpdGetMethod task simply iterates through all parsed account states associated with the specified contract name + // and re-execute the changed get method. + UpdGetMethod RescanTaskType = "upd_get_method" + + // UpdOperation task parses contract messages. + // It iterates through all messages with specified operation id, + // directed to (or originating from, in the case of outgoing operations) the given contract + // and adds the parsed data. + UpdOperation RescanTaskType = "upd_operation" + + // DelOperation task is the same algorithm, as UpdOperation, but it removes the parsed data. + DelOperation RescanTaskType = "del_operation" +) + +type RescanTask struct { + bun.BaseModel `bun:"table:rescan_tasks" json:"-"` + + ID int `bun:",pk,autoincrement"` + Finished bool `bun:"finished,notnull"` + Type RescanTaskType `bun:"type:rescan_task_type,notnull"` + + // contract being rescanned + ContractName abi.ContractName `bun:",notnull" json:"contract_name"` + Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` + + // for get-method update + ChangedGetMethod string `json:"changed_get_methods,omitempty"` + + // for operations + MessageType MessageType `json:"message_type,omitempty"` + Outgoing bool `json:"outgoing,omitempty"` // if operation is going from contract + OperationID uint32 `json:"operation_id,omitempty"` + Operation *ContractOperation `bun:"rel:has-one,join:contract_name=contract_name,join:outgoing=outgoing,join:operation_id=operation_id" json:"contract_operation"` + + // checkpoint + LastAddress *addr.Address `bun:"type:bytea" json:"last_address"` + LastTxLt uint64 `bun:"type:bigint" json:"last_tx_lt"` +} + +type RescanRepository interface { + AddRescanTask(ctx context.Context, task *RescanTask) error + GetUnfinishedRescanTask(context.Context) (bun.Tx, *RescanTask, error) + SetRescanTask(context.Context, bun.Tx, *RescanTask) error +} From bdf89805189b29376a642e6716c75ff009c3226f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 19:11:08 +0700 Subject: [PATCH 117/186] [rescan] migrate to separate rescan repository --- cmd/rescan/rescan.go | 2 ++ internal/app/rescan.go | 1 + internal/app/rescan/rescan.go | 31 +++++++++++-------------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/cmd/rescan/rescan.go b/cmd/rescan/rescan.go index 67521f72..f6b9e202 100644 --- a/cmd/rescan/rescan.go +++ b/cmd/rescan/rescan.go @@ -22,6 +22,7 @@ import ( "github.com/tonindexer/anton/internal/core/repository/block" "github.com/tonindexer/anton/internal/core/repository/contract" "github.com/tonindexer/anton/internal/core/repository/msg" + rescanRepository "github.com/tonindexer/anton/internal/core/repository/rescan" ) var Command = &cli.Command{ @@ -80,6 +81,7 @@ var Command = &cli.Command{ }) i := rescan.NewService(&app.RescanConfig{ ContractRepo: contractRepo, + RescanRepo: rescanRepository.NewRepository(conn.PG), AccountRepo: account.NewRepository(conn.CH, conn.PG), BlockRepo: block.NewRepository(conn.CH, conn.PG), MessageRepo: msg.NewRepository(conn.CH, conn.PG), diff --git a/internal/app/rescan.go b/internal/app/rescan.go index 782fbc6e..c5c144fd 100644 --- a/internal/app/rescan.go +++ b/internal/app/rescan.go @@ -7,6 +7,7 @@ import ( type RescanConfig struct { ContractRepo core.ContractRepository + RescanRepo core.RescanRepository BlockRepo repository.Block AccountRepo repository.Account MessageRepo repository.Message diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index e298701a..423451e4 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -74,7 +74,7 @@ func (s *Service) rescanLoop() { defer s.wg.Done() for s.running() { - tx, task, err := s.ContractRepo.GetUnfinishedRescanTask(context.Background()) + tx, task, err := s.RescanRepo.GetUnfinishedRescanTask(context.Background()) if err != nil { if !errors.Is(err, core.ErrNotFound) { log.Error().Err(err).Msg("get rescan task") @@ -92,7 +92,7 @@ func (s *Service) rescanLoop() { continue } - if err := s.ContractRepo.SetRescanTask(context.Background(), tx, task); err != nil { + if err := s.RescanRepo.SetRescanTask(context.Background(), tx, task); err != nil { log.Error().Err(err).Msg("update rescan task") time.Sleep(time.Second) continue @@ -101,17 +101,17 @@ func (s *Service) rescanLoop() { } func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { - switch task.Type { - case core.AddInterface: - var codeHash []byte - if task.Contract.Code != nil { - codeCell, err := cell.FromBOC(task.Contract.Code) - if err != nil { - return errors.Wrapf(err, "making %s code cell from boc", task.Contract.Name) - } - codeHash = codeCell.Hash() + var codeHash []byte + if task.Contract != nil && task.Contract.Code != nil { + codeCell, err := cell.FromBOC(task.Contract.Code) + if err != nil { + return errors.Wrapf(err, "making %s code cell from boc", task.Contract.Name) } + codeHash = codeCell.Hash() + } + switch task.Type { + case core.AddInterface: ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, "", task.Contract.Addresses, codeHash, task.Contract.GetMethodHashes, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { return errors.Wrapf(err, "match states by interface description") @@ -144,15 +144,6 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro return nil case core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod: - var codeHash []byte - if task.Contract.Code != nil { - codeCell, err := cell.FromBOC(task.Contract.Code) - if err != nil { - return errors.Wrapf(err, "making %s code cell from boc", task.Contract.Name) - } - codeHash = codeCell.Hash() - } - ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, task.ContractName, task.Contract.Addresses, codeHash, task.Contract.GetMethodHashes, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { return errors.Wrapf(err, "match states by interface description") From 26de0cbde1c51209f2188eaffee35e494bd0d58e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:01:16 +0700 Subject: [PATCH 118/186] [cmd] contract: refactor command for partial rescan --- cmd/contract/interface.go | 463 ++++++++++++++++++++++++++++++-------- 1 file changed, 365 insertions(+), 98 deletions(-) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index 7f30d78e..725b85f9 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -1,6 +1,7 @@ package contract import ( + "context" "database/sql" "encoding/base64" "encoding/json" @@ -8,6 +9,7 @@ import ( "io" "math/big" "os" + "reflect" "strconv" "strings" @@ -22,8 +24,24 @@ import ( "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository/contract" + "github.com/tonindexer/anton/internal/core/repository/rescan" ) +func dbConnect() (*bun.DB, error) { + pg := bun.NewDB( + sql.OpenDB( + pgdriver.NewConnector( + pgdriver.WithDSN(env.GetString("DB_PG_URL", "")), + ), + ), + pgdialect.New(), + ) + if err := pg.Ping(); err != nil { + return nil, errors.Wrapf(err, "cannot ping postgresql") + } + return pg, nil +} + func readStdin() ([]*abi.InterfaceDesc, error) { var interfaces []*abi.InterfaceDesc @@ -139,6 +157,8 @@ func parseInterfaceDesc(d *abi.InterfaceDesc) (*core.ContractInterface, []*core. operations = append(operations, op) } + i.Operations = operations + return &i, operations, nil } @@ -164,46 +184,194 @@ func parseInterfacesDesc(descriptors []*abi.InterfaceDesc) (retD map[abi.TLBType return } -var Command = &cli.Command{ - Name: "contract", - Usage: "Adds contract interface to the database", +func diffDefinitions(ctx context.Context, contractRepo core.ContractRepository, current map[abi.TLBType]abi.TLBFieldsDesc) (added, changed map[abi.TLBType]abi.TLBFieldsDesc, err error) { + old, err := contractRepo.GetDefinitions(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "get definitions") + } - ArgsUsage: "[file1.json] [file2.json]", + added, changed = map[abi.TLBType]abi.TLBFieldsDesc{}, map[abi.TLBType]abi.TLBFieldsDesc{} + for dt, d := range current { + od, ok := old[dt] + if !ok { + added[dt] = d + } + if !reflect.DeepEqual(od, d) { + changed[dt] = d + } + } - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "stdin", - Usage: "read from stdin instead of files", - Aliases: []string{"i"}, - }, - }, + return added, changed, nil +} + +func diffSlices[V any](oldS, newS []V, getName func(v V) string) (added, changed, deleted []V) { + oldM, newM := map[string]V{}, map[string]V{} + for _, v := range oldS { + oldM[getName(v)] = v + } + for _, v := range newS { + newM[getName(v)] = v + } + + for vn, v := range newM { + ov, ok := oldM[vn] + if !ok { + added = append(added, v) + } + if !reflect.DeepEqual(ov, v) { + changed = append(changed, v) + } + } + for vn := range oldM { + _, ok := newM[vn] + if !ok { + deleted = append(deleted, oldM[vn]) + } + } + + return added, changed, deleted +} + +func diffInterface(oldInterface, newInterface *core.ContractInterface) (interfaceChanged bool, added, changed, deleted []abi.GetMethodDesc) { + interfaceChanged = !reflect.DeepEqual(newInterface.Addresses, oldInterface.Addresses) || + !reflect.DeepEqual(newInterface.Code, oldInterface.Code) || + !reflect.DeepEqual(newInterface.GetMethodHashes, oldInterface.GetMethodHashes) + + added, changed, deleted = diffSlices(oldInterface.GetMethodsDesc, newInterface.GetMethodsDesc, func(v abi.GetMethodDesc) string { return v.Name }) + + return interfaceChanged, added, changed, deleted +} + +func diffOperations(oldOperations, newOperations []*core.ContractOperation) (added, changed, deleted []*core.ContractOperation) { + return diffSlices(oldOperations, newOperations, func(v *core.ContractOperation) string { return v.OperationName }) +} + +func rescanGetMethod(ctx context.Context, in abi.ContractName, repo core.RescanRepository, t core.RescanTaskType, gm string) error { + err := repo.AddRescanTask(ctx, &core.RescanTask{ + Type: t, + ContractName: in, + ChangedGetMethod: gm, + }) + if err != nil { + return errors.Wrapf(err, "add rescan task for '%s' get-method", gm) + } + + log.Info(). + Str("rescan_type", string(t)). + Str("interface_name", string(in)). + Str("get_method", gm). + Msg("added get-method rescan task") + + return nil +} + +func rescanOperation(ctx context.Context, repo core.RescanRepository, t core.RescanTaskType, op *core.ContractOperation) error { + err := repo.AddRescanTask(ctx, &core.RescanTask{ + Type: t, + ContractName: op.ContractName, + MessageType: op.MessageType, + Outgoing: op.Outgoing, + OperationID: op.OperationID, + }) + if err != nil { + return errors.Wrapf(err, "add rescan task for '%s' operation", op.OperationName) + } + + log.Info(). + Str("rescan_type", string(t)). + Str("interface_name", string(op.ContractName)). + Str("operation_name", op.OperationName). + Msg("added operation rescan task") + + return nil +} + +var Command = &cli.Command{ + Name: "contract", + Usage: "Manages contract interfaces in the database", Subcommands: cli.Commands{ { - Name: "delete", - Usage: "Deletes contract interface from the database", + Name: "addInterfaces", + Usage: "Adds contract interface", + + ArgsUsage: "[file1.json] [file2.json]", + + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "stdin", + Usage: "read from stdin instead of files", + Aliases: []string{"i"}, + }, + }, - ArgsUsage: "[interface_name_1] [interface_name_2]", + Action: func(ctx *cli.Context) (err error) { + var interfacesDesc []*abi.InterfaceDesc - Action: func(ctx *cli.Context) error { - pg := bun.NewDB( - sql.OpenDB( - pgdriver.NewConnector( - pgdriver.WithDSN(env.GetString("DB_PG_URL", "")), - ), - ), - pgdialect.New(), - ) - if err := pg.Ping(); err != nil { - return errors.Wrapf(err, "cannot ping postgresql") + if ctx.Bool("stdin") { + interfacesDesc, err = readStdin() + } else { + filenames := ctx.Args().Slice() + if len(filenames) == 0 { + cli.ShowSubcommandHelpAndExit(ctx, 1) + } + interfacesDesc, err = readFiles(filenames) + } + if err != nil { + return err + } + + definitions, interfaces, operations, err := parseInterfacesDesc(interfacesDesc) + if err != nil { + return err + } + + pg, err := dbConnect() + if err != nil { + return err } contractRepo := contract.NewRepository(pg) + rescanRepo := rescan.NewRepository(pg) - for _, i := range ctx.Args().Slice() { - err := contractRepo.DelInterface(ctx.Context, i) + for dn, d := range definitions { + if err := contractRepo.AddDefinition(ctx.Context, dn, d); err != nil { + log.Error().Err(err).Str("definition_name", string(dn)).Msg("cannot insert contract definition") + } + } + for _, i := range interfaces { + if err := contractRepo.AddInterface(ctx.Context, i); err != nil { + log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot insert contract interface") + continue + } + err := rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ + Type: core.AddInterface, + ContractName: i.Name, + }) + if err != nil { + log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot add interface rescan task") + } + } + for _, op := range operations { + if err := contractRepo.AddOperation(ctx.Context, op); err != nil { + log.Error().Err(err). + Str("interface_name", string(op.ContractName)). + Str("operation_name", op.OperationName). + Msg("cannot insert contract operation") + continue + } + err := rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ + Type: core.UpdOperation, + ContractName: op.ContractName, + MessageType: op.MessageType, + Outgoing: op.Outgoing, + OperationID: op.OperationID, + }) if err != nil { - return errors.Wrapf(err, "deleting %s interface", i) + log.Error().Err(err). + Str("interface_name", string(op.ContractName)). + Str("op_name", op.OperationName). + Msg("cannot add operation rescan task") } } @@ -211,96 +379,195 @@ var Command = &cli.Command{ }, }, { - Name: "rescan", - Usage: "Updates account states and messages data from the given block", + Name: "updateInterface", + Usage: "Updates contract interface in the database and adds rescan tasks for the difference between old and new interfaces", + + ArgsUsage: "[file1.json] [file2.json]", + + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "stdin", + Usage: "read from stdin instead of files", + Aliases: []string{"i"}, + }, + &cli.StringFlag{ + Name: "contract-name", + Usage: "contract interface for update", + Aliases: []string{"c"}, + Required: true, + }, + }, - ArgsUsage: "[from_block]", + Action: func(ctx *cli.Context) (err error) { + var interfacesDesc []*abi.InterfaceDesc - Action: func(ctx *cli.Context) error { - pg := bun.NewDB( - sql.OpenDB( - pgdriver.NewConnector( - pgdriver.WithDSN(env.GetString("DB_PG_URL", "")), - ), - ), - pgdialect.New(), - ) - if err := pg.Ping(); err != nil { - return errors.Wrapf(err, "cannot ping postgresql") + if ctx.Bool("stdin") { + interfacesDesc, err = readStdin() + } else { + filenames := ctx.Args().Slice() + if len(filenames) == 0 { + cli.ShowSubcommandHelpAndExit(ctx, 1) + } + interfacesDesc, err = readFiles(filenames) + } + if err != nil { + return err + } + + definitions, interfaces, _, err := parseInterfacesDesc(interfacesDesc) + if err != nil { + return err + } + + contractName := abi.ContractName(ctx.String("contract-name")) + if contractName == "" { + return errors.Wrap(core.ErrInvalidArg, "contract interface name is not set") + } + + var newInterface *core.ContractInterface + for _, i := range interfaces { + if i.Name == contractName { + newInterface = i + } + } + if newInterface == nil { + return errors.Wrapf(core.ErrInvalidArg, "contract interface '%s' is found in abi description", contractName) + } + + pg, err := dbConnect() + if err != nil { + return err } contractRepo := contract.NewRepository(pg) + rescanRepo := rescan.NewRepository(pg) - fromBlock, err := strconv.ParseUint(ctx.Args().First(), 10, 32) + oldInterface, err := contractRepo.GetInterface(ctx.Context, contractName) if err != nil { - return errors.Wrap(err, "wrong from_block argument") + return errors.Wrapf(err, "get '%s' interface", newInterface.Name) } - err = contractRepo.CreateNewRescanTask(ctx.Context, uint32(fromBlock)) + addedDef, changedDef, err := diffDefinitions(ctx.Context, contractRepo, definitions) if err != nil { - if errors.Is(err, core.ErrAlreadyExists) { - return errors.New("there is already one unfinished task") + return err + } + for dn, d := range changedDef { + if err := contractRepo.UpdateDefinition(ctx.Context, dn, d); err != nil { + return errors.Wrapf(err, "cannot update contract definition '%s'", dn) + } + } + for dn, d := range addedDef { + if err := contractRepo.AddDefinition(ctx.Context, dn, d); err != nil { + return errors.Wrapf(err, "cannot insert contract definition '%s'", dn) + } + } + + iChanged, addedGm, changedGm, deletedGm := diffInterface(oldInterface, newInterface) + if iChanged { + if err := contractRepo.UpdateInterface(ctx.Context, newInterface); err != nil { + return errors.Wrapf(err, "cannot update contract interface '%s'", newInterface.Name) + } + } + + addedOp, changedOp, deletedOp := diffOperations(oldInterface.Operations, newInterface.Operations) + for _, op := range deletedOp { + if err := contractRepo.DeleteOperation(ctx.Context, op.OperationName); err != nil { + return errors.Wrapf(err, "cannot delete contract operation '%s'", op.OperationName) + } + } + for _, op := range changedOp { + if err := contractRepo.UpdateOperation(ctx.Context, op); err != nil { + return errors.Wrapf(err, "cannot update contract operation '%s'", op.OperationName) + } + } + for _, op := range addedOp { + if err := contractRepo.AddOperation(ctx.Context, op); err != nil { + return errors.Wrapf(err, "cannot insert contract operation '%s'", op.OperationName) + } + } + + for _, gm := range addedGm { + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.AddGetMethod, gm.Name); err != nil { + return err + } + } + for _, gm := range changedGm { + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.UpdGetMethod, gm.Name); err != nil { + return err + } + } + for _, gm := range deletedGm { + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.DelGetMethod, gm.Name); err != nil { + return err + } + } + + for _, op := range deletedOp { + if err := rescanOperation(ctx.Context, rescanRepo, core.DelOperation, op); err != nil { + return err + } + } + for _, op := range append(addedOp, changedOp...) { + if err := rescanOperation(ctx.Context, rescanRepo, core.UpdOperation, op); err != nil { + return err } - return errors.Wrapf(err, "create new rescan task") } return nil }, }, - }, + { + Name: "deleteInterface", + Usage: "Deletes contract interface from the database and removes associated parsed data", + + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "contract-name", + Usage: "contract interface for deletion", + Aliases: []string{"c"}, + Required: true, + }, + }, - Action: func(ctx *cli.Context) (err error) { - var interfacesDesc []*abi.InterfaceDesc - - if ctx.Bool("stdin") { - interfacesDesc, err = readStdin() - } else { - filenames := ctx.Args().Slice() - if len(filenames) == 0 { - cli.ShowSubcommandHelpAndExit(ctx, 1) - } - interfacesDesc, err = readFiles(filenames) - } - if err != nil { - return err - } + Action: func(ctx *cli.Context) (err error) { + contractName := abi.ContractName(ctx.String("contract-name")) + if contractName == "" { + return errors.Wrap(core.ErrInvalidArg, "contract interface name is not set") + } - definitions, interfaces, operations, err := parseInterfacesDesc(interfacesDesc) - if err != nil { - return err - } + pg, err := dbConnect() + if err != nil { + return err + } - pg := bun.NewDB( - sql.OpenDB( - pgdriver.NewConnector( - pgdriver.WithDSN(env.GetString("DB_PG_URL", "")), - ), - ), - pgdialect.New(), - ) - if err := pg.Ping(); err != nil { - return errors.Wrapf(err, "cannot ping postgresql") - } + contractRepo := contract.NewRepository(pg) + rescanRepo := rescan.NewRepository(pg) - for dn, d := range definitions { - if err := contract.NewRepository(pg).AddDefinition(ctx.Context, dn, d); err != nil { - log.Err(err).Str("definition_name", string(dn)).Msg("cannot insert contract interface") - } - } - for _, i := range interfaces { - if err := contract.NewRepository(pg).AddInterface(ctx.Context, i); err != nil { - log.Err(err).Str("interface_name", string(i.Name)).Msg("cannot insert contract interface") - } - } - for _, op := range operations { - if err := contract.NewRepository(pg).AddOperation(ctx.Context, op); err != nil { - log.Err(err). - Str("interface_name", string(op.ContractName)). - Str("operation_name", op.OperationName). - Msg("cannot insert contract operation") - } - } + oldInterface, err := contractRepo.GetInterface(ctx.Context, contractName) + if err != nil { + return errors.Wrapf(err, "get '%s' interface", oldInterface.Name) + } + + if err := contractRepo.DeleteInterface(ctx.Context, contractName); err != nil { + return errors.Wrapf(err, "cannot delete '%s' interface", contractName) + } - return nil + for _, op := range oldInterface.Operations { + if err := rescanOperation(ctx.Context, rescanRepo, core.DelOperation, op); err != nil { + return err + } + } + + err = rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ + Type: core.DelInterface, + ContractName: contractName, + }) + if err != nil { + log.Error().Err(err).Str("interface_name", string(contractName)).Msg("cannot add interface rescan task") + } + + return nil + }, + }, }, } From 0c479170b9b2c36da48d47ca2882326bcccee880 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:31:26 +0700 Subject: [PATCH 119/186] [parser] fix lint warnings --- internal/app/parser/account.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index 27905cea..b9210eb7 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -149,9 +149,7 @@ func (s *Service) ParseAccountContractData( if acc.ExecutedGetMethods == nil { acc.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} } - if _, ok := acc.ExecutedGetMethods[contractDesc.Name]; ok { - delete(acc.ExecutedGetMethods, contractDesc.Name) - } + delete(acc.ExecutedGetMethods, contractDesc.Name) s.callPossibleGetMethods(ctx, acc, others, []*core.ContractInterface{contractDesc}) @@ -190,8 +188,9 @@ func (s *Service) ExecuteAccountGetMethod( return errors.Wrapf(core.ErrInvalidArg, "cannot find '%s' interface description for '%s' account", contract, acc.Address) } - for _, d = range i.GetMethodsDesc { - if d.Name == getMethod { + for it := range i.GetMethodsDesc { + if i.GetMethodsDesc[it].Name == getMethod { + d = &i.GetMethodsDesc[it] break } } From ee5f181118dc6849205c58d8169211efb69fdd45 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:31:53 +0700 Subject: [PATCH 120/186] [parser] delete test for checkPrevGetMethodExecution --- internal/app/parser/get_test.go | 312 -------------------------------- 1 file changed, 312 deletions(-) delete mode 100644 internal/app/parser/get_test.go diff --git a/internal/app/parser/get_test.go b/internal/app/parser/get_test.go deleted file mode 100644 index 04869f48..00000000 --- a/internal/app/parser/get_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package parser - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "math/big" - "testing" - - "github.com/stretchr/testify/require" - "github.com/xssnick/tonutils-go/tvm/cell" - - "github.com/tonindexer/anton/abi" - "github.com/tonindexer/anton/addr" - "github.com/tonindexer/anton/internal/core" -) - -func mustFromB64(t *testing.T, b64 string) []byte { - s, err := base64.StdEncoding.DecodeString(b64) - require.NoError(t, err) - return s -} - -func mustFromBOC(t *testing.T, b64 string) *cell.Cell { - s, err := base64.StdEncoding.DecodeString(b64) - require.NoError(t, err) - c, err := cell.FromBOC(s) - require.NoError(t, err) - return c -} - -func TestService_checkPrevGetMethodExecution(t *testing.T) { - var testCases = []struct { - contract abi.ContractName - descJson string - executedJson string - args []any - result bool - }{ - { - contract: "nft_collection", - descJson: ` -{ - "name": "get_nft_content", - "arguments": [ - { - "name": "index", - "stack_type": "int" - }, - { - "name": "individual_content", - "stack_type": "cell" - } - ], - "return_values": [ - { - "name": "full_content", - "stack_type": "cell", - "format": "content" - } - ] -}`, - executedJson: ` -{ - "name": "get_nft_content", - "arguments": [ - { - "name": "index", - "stack_type": "int" - }, - { - "name": "individual_content", - "stack_type": "cell" - } - ], - "receives": [ - 1.11966e+5, - "te6cckEBAQEAMwAAYgFodHRwczovL25mdC5mcmFnbWVudC5jb20vbnVtYmVyLzg4ODA4MTk1NzU0Lmpzb26DXfO0" - ], - "return_values": [ - { - "name": "full_content", - "stack_type": "cell", - "format": "content" - } - ], - "returns": [ - { - "URI": "https://nft.fragment.com/number/88808195754.json" - } - ] -}`, - args: []any{big.NewInt(111966), mustFromBOC(t, "te6cckEBAQEAMwAAYgFodHRwczovL25mdC5mcmFnbWVudC5jb20vbnVtYmVyLzg4ODA4MTk1NzU0Lmpzb26DXfO0")}, - result: true, - }, - { - contract: "nft_collection", - descJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int", - "format": "bytes" - } - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ] -}`, - executedJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int" - } - ], - "receives": [ - 10 - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ], - "returns": [ - "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" - ] -}`, - args: []any{big.NewInt(10)}, - result: false, - }, - { - contract: "nft_collection", - descJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int" - } - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ] -}`, - executedJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int" - } - ], - "receives": [ - 10 - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ], - "returns": [ - "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" - ] -}`, - args: []any{big.NewInt(10)}, - result: true, - }, - { - contract: "nft_collection", - descJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int", - "format": "bytes" - } - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ] -}`, - executedJson: ` -{ - "name": "get_nft_address_by_index", - "arguments": [ - { - "name": "index", - "stack_type": "int", - "format": "bytes" - } - ], - "receives": [ - "08tzIyyFysK97F6dtnqpSZkuqqKit6y2gPvelDuXoPQ=" - ], - "return_values": [ - { - "name": "address", - "stack_type": "slice", - "format": "addr" - } - ], - "returns": [ - "EQDHVwNhkIvqS3tJf0ScpM2kGd0Yi0PgGf_lZ1Vh0m7AyWD3" - ] -}`, - args: []any{mustFromB64(t, "08tzIyyFysK97F6dtnqpSZkuqqKit6y2gPvelDuXoPQ=")}, - result: false, - }, - { - contract: "jetton_minter", - descJson: ` -{ - "name": "get_wallet_address", - "arguments": [ - { - "name": "owner_address", - "stack_type": "slice", - "format": "addr" - } - ], - "return_values": [ - { - "name": "wallet_address", - "stack_type": "slice", - "format": "addr" - } - ] -}`, - executedJson: ` -{ - "name": "get_wallet_address", - "arguments": [ - { - "name": "owner_address", - "stack_type": "slice", - "format": "addr" - } - ], - "receives": [ - "EQDwKGXmxr9kV9bxUoi3o4eU9o6onrKw3g2sd57XAZFWV_kw" - ], - "return_values": [ - { - "name": "wallet_address", - "stack_type": "slice", - "format": "addr" - } - ], - "returns": [ - "EQAdeuTxkNGycqRS-MdwRGVtnEjiS1p7quWaA36Q2XlnOa4Q" - ] -}`, - args: []any{addr.MustFromBase64("EQDwKGXmxr9kV9bxUoi3o4eU9o6onrKw3g2sd57XAZFWV_kw").MustToTonutils()}, - result: true, - }, - } - - for it := range testCases { - ts := &testCases[it] - - var ( - desc abi.GetMethodDesc - exec abi.GetMethodExecution - ) - - err := json.Unmarshal([]byte(ts.descJson), &desc) - require.Nil(t, err) - - err = json.Unmarshal([]byte(ts.executedJson), &exec) - require.Nil(t, err) - - id, res := (*Service)(nil).checkPrevGetMethodExecution( - ts.contract, - &desc, - &core.AccountState{ - ExecutedGetMethods: map[abi.ContractName][]abi.GetMethodExecution{ - ts.contract: {exec}, - }, - }, - ts.args, - ) - require.Equal(t, ts.result, res, fmt.Sprintf("test number %d", it)) - if res { - require.Equal(t, 0, id) - } - } -} From cd45b3d84d84aaef26d3e69040fcbffc52d71b82 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:32:11 +0700 Subject: [PATCH 121/186] [parser] fix mockContractRepo interface --- internal/app/parser/parser_test.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index adf8b913..77c3a8d1 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/uptrace/bun" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" @@ -38,36 +37,48 @@ type mockContractRepo struct { func (m *mockContractRepo) AddDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error { panic("implement me") } +func (m *mockContractRepo) UpdateDefinition(context.Context, abi.TLBType, abi.TLBFieldsDesc) error { + panic("implement me") +} +func (m *mockContractRepo) DeleteDefinition(context.Context, abi.TLBType) error { + panic("implement me") +} func (m *mockContractRepo) GetDefinitions(context.Context) (map[abi.TLBType]abi.TLBFieldsDesc, error) { panic("implement me") } + func (m *mockContractRepo) AddInterface(_ context.Context, _ *core.ContractInterface) error { panic("implement me") } -func (m *mockContractRepo) AddOperation(_ context.Context, _ *core.ContractOperation) error { +func (m *mockContractRepo) UpdateInterface(context.Context, *core.ContractInterface) error { panic("implement me") } -func (m *mockContractRepo) DelInterface(_ context.Context, _ string) error { +func (m *mockContractRepo) DeleteInterface(context.Context, abi.ContractName) error { panic("implement me") } func (m *mockContractRepo) GetInterfaces(_ context.Context) ([]*core.ContractInterface, error) { return m.interfaces, nil } +func (m *mockContractRepo) GetInterface(context.Context, abi.ContractName) (*core.ContractInterface, error) { + panic("implement me") +} func (m *mockContractRepo) GetMethodDescription(context.Context, abi.ContractName, string) (abi.GetMethodDesc, error) { panic("implement me") } -func (m *mockContractRepo) GetOperations(_ context.Context) ([]*core.ContractOperation, error) { + +func (m *mockContractRepo) AddOperation(_ context.Context, _ *core.ContractOperation) error { panic("implement me") } -func (m *mockContractRepo) GetOperationByID(_ context.Context, _ core.MessageType, _ []abi.ContractName, _ bool, _ uint32) (*core.ContractOperation, error) { +func (m *mockContractRepo) UpdateOperation(context.Context, *core.ContractOperation) error { panic("implement me") } - -func (m *mockContractRepo) GetUnfinishedRescanTask(_ context.Context) (bun.Tx, *core.RescanTask, error) { +func (m *mockContractRepo) DeleteOperation(context.Context, string) error { panic("implement me") } - -func (m *mockContractRepo) SetRescanTask(_ context.Context, _ bun.Tx, _ *core.RescanTask) error { +func (m *mockContractRepo) GetOperations(_ context.Context) ([]*core.ContractOperation, error) { + panic("implement me") +} +func (m *mockContractRepo) GetOperationByID(_ context.Context, _ core.MessageType, _ []abi.ContractName, _ bool, _ uint32) (*core.ContractOperation, error) { panic("implement me") } From ca6832b7fd5aafea00863a3be341cd9334bf2929 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:32:59 +0700 Subject: [PATCH 122/186] [repo] move rescan repo tests from contract repo --- internal/core/repository/contract/contract.go | 25 --- .../core/repository/contract/contract_test.go | 112 +---------- internal/core/repository/rescan/rescan.go | 23 ++- .../core/repository/rescan/rescan_test.go | 180 ++++++++++++++++++ 4 files changed, 203 insertions(+), 137 deletions(-) create mode 100644 internal/core/repository/rescan/rescan_test.go diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index fcb6e6d9..3c95562e 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -52,21 +52,6 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "contract interface pg create table") } - _, err = pgDB.ExecContext(ctx, "CREATE TYPE rescan_task_type AS ENUM (?, ?, ?, ?, ?, ?, ?, ?)", - core.AddInterface, core.UpdInterface, core.DelInterface, core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod, core.UpdOperation, core.DelOperation) - if err != nil && !strings.Contains(err.Error(), "already exists") { - return errors.Wrap(err, "rescan task type pg create enum") - } - - _, err = pgDB.NewCreateTable(). - Model(&core.RescanTask{}). - IfNotExists(). - // WithForeignKeys(). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "rescan task pg create table") - } - _, err = pgDB.NewCreateIndex(). Model(&core.ContractInterface{}). Unique(). @@ -82,16 +67,6 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { return errors.Wrap(err, "messages pg create source tx hash check") } - _, err = pgDB.NewCreateIndex(). - Model(&core.RescanTask{}). - Unique(). - Column("finished"). - Where("finished = false"). - Exec(ctx) - if err != nil { - return errors.Wrap(err, "rescan task finished create unique index") - } - return nil } diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 1b2e5ad5..35b5f75f 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" @@ -51,9 +50,7 @@ func dropTables(t testing.TB) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - _, err := pg.NewDropTable().Model((*core.RescanTask)(nil)).IfExists().Exec(ctx) - require.Nil(t, err) - _, err = pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) + _, err := pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) require.Nil(t, err) _, err = pg.NewDropTable().Model((*core.ContractInterface)(nil)).IfExists().Exec(ctx) require.Nil(t, err) @@ -236,110 +233,3 @@ func TestRepository_AddContracts(t *testing.T) { dropTables(t) }) } - -func TestRepository_CreateNewRescanTask(t *testing.T) { - initdb(t) - - i := &core.ContractInterface{ - Name: known.NFTItem, - Addresses: []*addr.Address{rndm.Address()}, - Code: rndm.Bytes(128), - GetMethodsDesc: []abi.GetMethodDesc{ - { - Name: "get_nft_content", - Arguments: []abi.VmValueDesc{ - { - Name: "index", - StackType: "int", - }, { - Name: "individual_content", - StackType: "cell", - }, - }, - ReturnValues: []abi.VmValueDesc{ - { - Name: "full_content", - StackType: "cell", - Format: "content", - }, - }, - }, - }, - GetMethodHashes: rndm.GetMethodHashes(), - } - - task := core.RescanTask{ - Type: core.AddInterface, - ContractName: known.NFTItem, - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - t.Run("drop tables", func(t *testing.T) { - dropTables(t) - }) - - t.Run("create tables", func(t *testing.T) { - createTables(t) - }) - - t.Run("insert interface", func(t *testing.T) { - err := repo.AddInterface(ctx, i) - require.Nil(t, err) - }) - - t.Run("create new task", func(t *testing.T) { - err := repo.CreateNewRescanTask(ctx, &task) - require.NoError(t, err) - }) - - t.Run("update unfinished task", func(t *testing.T) { - tx, task, err := repo.GetUnfinishedRescanTask(ctx) - require.NoError(t, err) - - task.LastAddress = i.Addresses[0] - task.LastTxLt = 10 - - err = repo.SetRescanTask(ctx, tx, task) - require.NoError(t, err) - }) - - t.Run("finish task", func(t *testing.T) { - tx, task, err := repo.GetUnfinishedRescanTask(ctx) - require.NoError(t, err) - require.Equal(t, i.Addresses[0], task.LastAddress) - require.Equal(t, uint64(10), task.LastTxLt) - - task.LastAddress = i.Addresses[0] - task.LastTxLt = 20 - task.Finished = true - - err = repo.SetRescanTask(ctx, tx, task) - require.NoError(t, err) - }) - - t.Run("get 'not found' error on choosing unfinished task", func(t *testing.T) { - _, _, err := repo.GetUnfinishedRescanTask(ctx) - require.Error(t, err) - require.True(t, errors.Is(err, core.ErrNotFound)) - }) - - t.Run("create second task", func(t *testing.T) { - err := repo.CreateNewRescanTask(ctx, &task) - require.NoError(t, err) - - tx, task, err := repo.GetUnfinishedRescanTask(ctx) - require.NoError(t, err) - require.Equal(t, 2, task.ID) - - task.Finished = true - - err = repo.SetRescanTask(ctx, tx, task) - require.NoError(t, err) - }) - - t.Run("drop tables", func(t *testing.T) { - dropTables(t) - }) -} diff --git a/internal/core/repository/rescan/rescan.go b/internal/core/repository/rescan/rescan.go index 384fa826..38c0958b 100644 --- a/internal/core/repository/rescan/rescan.go +++ b/internal/core/repository/rescan/rescan.go @@ -3,6 +3,7 @@ package rescan import ( "context" "database/sql" + "strings" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -20,7 +21,27 @@ func NewRepository(db *bun.DB) *Repository { return &Repository{pg: db} } +func CreateTables(ctx context.Context, pgDB *bun.DB) error { + _, err := pgDB.ExecContext(ctx, "CREATE TYPE rescan_task_type AS ENUM (?, ?, ?, ?, ?, ?, ?, ?)", + core.AddInterface, core.UpdInterface, core.DelInterface, core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod, core.UpdOperation, core.DelOperation) + if err != nil && !strings.Contains(err.Error(), "already exists") { + return errors.Wrap(err, "rescan task type pg create enum") + } + + _, err = pgDB.NewCreateTable(). + Model(&core.RescanTask{}). + IfNotExists(). + // WithForeignKeys(). + Exec(ctx) + if err != nil { + return errors.Wrap(err, "rescan task pg create table") + } + + return nil +} + func (r *Repository) AddRescanTask(ctx context.Context, task *core.RescanTask) error { + task.ID = 0 _, err := r.pg.NewInsert().Model(task).Exec(ctx) if err != nil { return err @@ -59,7 +80,7 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core if errors.Is(err, sql.ErrNoRows) { err = core.ErrNotFound } - return bun.Tx{}, nil, errors.Wrapf(core.ErrNotFound, "no %s contract interface for %s task", task.ContractName, task.Type) + return bun.Tx{}, nil, errors.Wrapf(err, "no %s contract interface for %s task", task.ContractName, task.Type) } } diff --git a/internal/core/repository/rescan/rescan_test.go b/internal/core/repository/rescan/rescan_test.go new file mode 100644 index 00000000..e1fe5b94 --- /dev/null +++ b/internal/core/repository/rescan/rescan_test.go @@ -0,0 +1,180 @@ +package rescan + +import ( + "context" + "database/sql" + "strings" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" + + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/known" + "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/repository/contract" + "github.com/tonindexer/anton/internal/core/rndm" +) + +var ( + pg *bun.DB + repo *Repository +) + +func initdb(t testing.TB) { + var ( + dsnPG = "postgres://user:pass@localhost:5432/postgres?sslmode=disable" + err error + ) + + pg = bun.NewDB(sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsnPG))), pgdialect.New()) + err = pg.Ping() + require.Nil(t, err) + + repo = NewRepository(pg) +} + +func createTables(t testing.TB) { + _, err := pg.ExecContext(context.Background(), "CREATE TYPE message_type AS ENUM (?, ?, ?)", core.ExternalIn, core.ExternalOut, core.Internal) + require.Nil(t, err) + err = contract.CreateTables(context.Background(), pg) + require.Nil(t, err) + err = CreateTables(context.Background(), pg) + require.Nil(t, err) +} + +func dropTables(t testing.TB) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + _, err := pg.NewDropTable().Model((*core.RescanTask)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.ContractOperation)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.ContractInterface)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + _, err = pg.NewDropTable().Model((*core.ContractDefinition)(nil)).IfExists().Exec(ctx) + require.Nil(t, err) + + _, err = pg.ExecContext(context.Background(), "DROP TYPE message_type") + if err != nil && !strings.Contains(err.Error(), "does not exist") { + t.Fatal(err) + } + + _, err = pg.ExecContext(context.Background(), "DROP TYPE rescan_task_type") + if err != nil && !strings.Contains(err.Error(), "does not exist") { + t.Fatal(err) + } +} + +func TestRepository_CreateNewRescanTask(t *testing.T) { + initdb(t) + + i := &core.ContractInterface{ + Name: known.NFTItem, + Addresses: []*addr.Address{rndm.Address()}, + Code: rndm.Bytes(128), + GetMethodsDesc: []abi.GetMethodDesc{ + { + Name: "get_nft_content", + Arguments: []abi.VmValueDesc{ + { + Name: "index", + StackType: "int", + }, { + Name: "individual_content", + StackType: "cell", + }, + }, + ReturnValues: []abi.VmValueDesc{ + { + Name: "full_content", + StackType: "cell", + Format: "content", + }, + }, + }, + }, + GetMethodHashes: rndm.GetMethodHashes(), + } + + task := core.RescanTask{ + Type: core.AddInterface, + ContractName: known.NFTItem, + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + t.Run("drop tables", func(t *testing.T) { + dropTables(t) + }) + + t.Run("create tables", func(t *testing.T) { + createTables(t) + }) + + t.Run("insert interface", func(t *testing.T) { + err := contract.NewRepository(pg).AddInterface(ctx, i) + require.Nil(t, err) + }) + + t.Run("create new task", func(t *testing.T) { + err := repo.AddRescanTask(ctx, &task) + require.NoError(t, err) + }) + + t.Run("update unfinished task", func(t *testing.T) { + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + + task.LastAddress = i.Addresses[0] + task.LastTxLt = 10 + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("finish task", func(t *testing.T) { + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + require.Equal(t, i.Addresses[0], task.LastAddress) + require.Equal(t, uint64(10), task.LastTxLt) + + task.LastAddress = i.Addresses[0] + task.LastTxLt = 20 + task.Finished = true + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("get 'not found' error on choosing unfinished task", func(t *testing.T) { + _, _, err := repo.GetUnfinishedRescanTask(ctx) + require.Error(t, err) + require.True(t, errors.Is(err, core.ErrNotFound)) + }) + + t.Run("create second task", func(t *testing.T) { + err := repo.AddRescanTask(ctx, &task) + require.NoError(t, err) + + tx, task, err := repo.GetUnfinishedRescanTask(ctx) + require.NoError(t, err) + require.Equal(t, 2, task.ID) + + task.Finished = true + + err = repo.SetRescanTask(ctx, tx, task) + require.NoError(t, err) + }) + + t.Run("drop tables", func(t *testing.T) { + dropTables(t) + }) +} From 070e7e2a21039b4612a9e4259f658caad68147a1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 20:43:09 +0700 Subject: [PATCH 123/186] [parser] fix tests --- internal/app/parser/account_test.go | 2 +- internal/app/parser/parser_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/app/parser/account_test.go b/internal/app/parser/account_test.go index bdb444e5..0728cda5 100644 --- a/internal/app/parser/account_test.go +++ b/internal/app/parser/account_test.go @@ -109,7 +109,7 @@ func TestService_ParseAccountData_NFTItem(t *testing.T) { require.Equal(t, "https://loton.fun/nft/100.json", ret.NFTContentData.ContentURI) j, err := json.Marshal(ret.ExecutedGetMethods) require.Nil(t, err) - require.Equal(t, `{"nft_collection":[{"name":"get_nft_content","receives":[100,"te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g=="],"returns":[{"URI":"https://loton.fun/nft/100.json"}]},{"name":"get_nft_address_by_index","receives":[100],"returns":["EQAQKmY9GTsEb6lREv-vxjT5sVHJyli40xGEYP3tKZSDuTBj"]}],"nft_item":[{"name":"get_nft_data","returns":[true,100,"EQBMy6CNgBk8PrT5LNjPELxCX_LXBaVSqtbzRToUHlG3t-fg","EQCIoWk-ZntpYQIRbcaME0ri29yWPEtbL-ay74AJy7KFlcfj","te6cckEBAQEACgAAEDEwMC5qc29ue9bV9g=="]}]}`, string(j)) + 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)) require.Equal(t, false, ret.Fake) } diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index 77c3a8d1..576a625b 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -127,6 +127,7 @@ func newService(t *testing.T) *Service { }, { Name: "index", StackType: "int", + Format: "bytes", }, { Name: "collection_address", StackType: "slice", From bd32ae234992aeb214c0622a3306991383961f6a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 21:39:43 +0700 Subject: [PATCH 124/186] [cmd] rescan: add select limit variable --- cmd/rescan/rescan.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/rescan/rescan.go b/cmd/rescan/rescan.go index f6b9e202..f746f101 100644 --- a/cmd/rescan/rescan.go +++ b/cmd/rescan/rescan.go @@ -87,6 +87,7 @@ var Command = &cli.Command{ MessageRepo: msg.NewRepository(conn.CH, conn.PG), Parser: p, Workers: env.GetInt("RESCAN_WORKERS", 4), + SelectLimit: env.GetInt("RESCAN_SELECT_LIMIT", 1000), }) if err = i.Start(); err != nil { return err From b5c3069357c964dd8bafbd3693172e617f78d0a3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 21:40:42 +0700 Subject: [PATCH 125/186] docker-compose.yml: add rescan limit env --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 64507a7a..57abac4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: environment: <<: *anton-env RESCAN_WORKERS: ${RESCAN_WORKERS} + RESCAN_RESCAN_LIMIT: ${RESCAN_SELECT_LIMIT} LITESERVERS: ${LITESERVERS} DEBUG_LOGS: ${DEBUG_LOGS} web: From 4cd18811b80813301b90d0cf1ee1c51d88cbee33 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 21:59:08 +0700 Subject: [PATCH 126/186] docker-compose.yml: fix typo --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 57abac4a..a18764dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,7 @@ services: environment: <<: *anton-env RESCAN_WORKERS: ${RESCAN_WORKERS} - RESCAN_RESCAN_LIMIT: ${RESCAN_SELECT_LIMIT} + RESCAN_SELECT_LIMIT: ${RESCAN_SELECT_LIMIT} LITESERVERS: ${LITESERVERS} DEBUG_LOGS: ${DEBUG_LOGS} web: From e6ccc04dab86a9e8301abd0153cb212c4d926bb2 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 21:59:37 +0700 Subject: [PATCH 127/186] .env.example: add rescan select limit variable --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 5b9641f4..aecebf46 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ LITESERVERS=135.181.177.59:53312|aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= DEBUG_LOGS=false WORKERS=4 RESCAN_WORKERS=4 +RESCAN_SELECT_LIMIT=1000 # LITESERVERS=65.108.141.177:17439|0MIADpLH4VQn+INHfm0FxGiuZZAA8JfTujRqQugkkA8= # testnet \ No newline at end of file From 0c51beb44fdff3deadaf5bc841ff1d52048c103a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 22:00:05 +0700 Subject: [PATCH 128/186] [migrations] update rescan table --- .../20240213085742_reindex_state.down.sql | 6 ++-- .../20240213085742_reindex_state.up.sql | 33 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/migrations/pgmigrations/20240213085742_reindex_state.down.sql b/migrations/pgmigrations/20240213085742_reindex_state.down.sql index e547ce5a..90fb596f 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.down.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.down.sql @@ -1,5 +1,7 @@ SET statement_timeout = 0; -DROP TABLE rescan_tasks; +BEGIN; + DROP TABLE rescan_tasks; -DROP INDEX account_states_workchain_shard_block_seq_no_idx; \ No newline at end of file + DROP TYPE rescan_task_type; +COMMIT; \ No newline at end of file diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql index be7d761c..4ef37a60 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.up.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -1,22 +1,37 @@ SET statement_timeout = 0; BEGIN; + CREATE TYPE rescan_task_type AS ENUM ( + 'add_interface', + 'upd_interface', + 'del_interface', + 'add_get_method', + 'del_get_method', + 'upd_get_method', + 'upd_operation', + 'del_operation' + ); + CREATE SEQUENCE rescan_tasks_id_seq START WITH 1; CREATE TABLE rescan_tasks ( id integer NOT NULL DEFAULT nextval('rescan_tasks_id_seq'), finished bool NOT NULL, - start_from_masterchain_seq_no integer NOT NULL, - accounts_last_masterchain_seq_no integer NOT NULL, - accounts_rescan_done boolean NOT NULL, - messages_last_masterchain_seq_no integer NOT NULL, - messages_rescan_done boolean NOT NULL, + type rescan_task_type NOT NULL, + + contract_name text NOT NULL, + + changed_get_method text NOT NULL, + + message_type message_type NOT NULL, + outgoing boolean NOT NULL, + operation_id integer NOT NULL, + + last_address bytea NOT NULL, + last_tx_lt bigint NOT NULL, + CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) ); ALTER SEQUENCE rescan_tasks_id_seq OWNED BY rescan_tasks.id; - - CREATE UNIQUE INDEX ON rescan_tasks (finished) WHERE finished = false; COMMIT; - -CREATE INDEX account_states_workchain_shard_block_seq_no_idx ON account_states USING btree (workchain, shard, block_seq_no); From a258e9c8b75e01c158bf4f99f88c70ebb94480aa Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 22:00:57 +0700 Subject: [PATCH 129/186] [core] rescan task: fix changed_get_method json tag --- internal/core/rescan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/rescan.go b/internal/core/rescan.go index 1a5ccc29..6a13a543 100644 --- a/internal/core/rescan.go +++ b/internal/core/rescan.go @@ -60,7 +60,7 @@ type RescanTask struct { Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` // for get-method update - ChangedGetMethod string `json:"changed_get_methods,omitempty"` + ChangedGetMethod string `json:"changed_get_method,omitempty"` // for operations MessageType MessageType `json:"message_type,omitempty"` From b4530e343faeb6a4fe2b9bf89da06f5f9591f31c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 22:28:09 +0700 Subject: [PATCH 130/186] [repo] contract: fix DeleteDefinition --- internal/core/repository/contract/contract.go | 2 +- internal/core/repository/contract/contract_test.go | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 3c95562e..54bfd67d 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -109,7 +109,7 @@ func (r *Repository) UpdateDefinition(ctx context.Context, dn abi.TLBType, d abi func (r *Repository) DeleteDefinition(ctx context.Context, dn abi.TLBType) error { def := &core.ContractDefinition{Name: dn} - ret, err := r.pg.NewUpdate().Model(def).WherePK().Exec(ctx) + ret, err := r.pg.NewDelete().Model(def).WherePK().Exec(ctx) if err != nil { return err } diff --git a/internal/core/repository/contract/contract_test.go b/internal/core/repository/contract/contract_test.go index 35b5f75f..747f028f 100644 --- a/internal/core/repository/contract/contract_test.go +++ b/internal/core/repository/contract/contract_test.go @@ -61,11 +61,6 @@ func dropTables(t testing.TB) { if err != nil && !strings.Contains(err.Error(), "does not exist") { t.Fatal(err) } - - _, err = pg.ExecContext(context.Background(), "DROP TYPE rescan_task_type") - if err != nil && !strings.Contains(err.Error(), "does not exist") { - t.Fatal(err) - } } func TestRepository_AddContracts(t *testing.T) { From 8938e3c90661688f38ef03af45c9bbaa9b74bcba Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 22:59:18 +0700 Subject: [PATCH 131/186] [rescan] clearParsedAccountsData: remove old contract type --- internal/app/rescan/account.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 51493a1f..beaa3384 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -50,15 +50,30 @@ func (s *Service) getRecentAccountState(ctx context.Context, b core.BlockID, a a func copyAccountState(state *core.AccountState) *core.AccountState { update := *state + + update.Types = make([]abi.ContractName, len(state.Types)) + copy(update.Types, state.Types) + update.ExecutedGetMethods = map[abi.ContractName][]abi.GetMethodExecution{} for n, e := range state.ExecutedGetMethods { update.ExecutedGetMethods[n] = make([]abi.GetMethodExecution, len(e)) copy(update.ExecutedGetMethods[n], e) } + return &update } func (s *Service) clearParsedAccountsData(task *core.RescanTask, acc *core.AccountState) { + for it := range acc.Types { + if acc.Types[it] != task.ContractName { + continue + } + types := acc.Types + copy(types[it:], types[it+1:]) + acc.Types = types[:len(types)-1] + break + } + _, ok := acc.ExecutedGetMethods[task.ContractName] if !ok { return From 818c0cab36be1dc018d5d0bc51d99bedfa96746a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 12 Mar 2024 23:03:56 +0700 Subject: [PATCH 132/186] [rescan] fix rescanMessages and rescanAccounts batching --- internal/app/rescan/account.go | 1 + internal/app/rescan/tx.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index beaa3384..c17c674e 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -283,6 +283,7 @@ func (s *Service) rescanAccounts(ctx context.Context, task *core.RescanTask, ids }(accounts[i : i+batchLen]) i += batchLen + workers-- } go func() { diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 64f7d3b3..15848df3 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -147,6 +147,7 @@ func (s *Service) rescanMessages(ctx context.Context, task *core.RescanTask, has }(messages[i : i+batchLen]) i += batchLen + workers-- } go func() { From 2cc13d453b5cb5c5470cb59cda700d9c89b4cda8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 14:32:33 +0700 Subject: [PATCH 133/186] [repo] contract: handle already exists error on adding --- internal/core/repository/contract/contract.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/core/repository/contract/contract.go b/internal/core/repository/contract/contract.go index 54bfd67d..07df2722 100644 --- a/internal/core/repository/contract/contract.go +++ b/internal/core/repository/contract/contract.go @@ -78,6 +78,9 @@ func (r *Repository) AddDefinition(ctx context.Context, dn abi.TLBType, d abi.TL _, err := r.pg.NewInsert().Model(def).Exec(ctx) if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return core.ErrAlreadyExists + } return err } @@ -144,6 +147,9 @@ func (r *Repository) GetDefinitions(ctx context.Context) (map[abi.TLBType]abi.TL func (r *Repository) AddInterface(ctx context.Context, i *core.ContractInterface) error { _, err := r.pg.NewInsert().Model(i).Exec(ctx) if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return core.ErrAlreadyExists + } return err } return nil @@ -245,6 +251,9 @@ func (r *Repository) GetMethodDescription(ctx context.Context, name abi.Contract func (r *Repository) AddOperation(ctx context.Context, op *core.ContractOperation) error { _, err := r.pg.NewInsert().Model(op).Exec(ctx) if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return core.ErrAlreadyExists + } return err } return nil From cd79660cd74ed53ef9d090e2a38096d466aa495a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 14:33:19 +0700 Subject: [PATCH 134/186] [repo] fix MatchStatesByInterfaceDesc and MatchMessagesByOperationDesc order expression --- internal/core/repository/account/account.go | 2 +- internal/core/repository/msg/msg.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index c2ded93e..c1886688 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -317,7 +317,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, q = q.Where("(address, after_tx_lt) > (?, ?)", afterAddress, afterTxLt) } err := q. - Order("address ASC, last_tx_lt ASC"). + OrderExpr("address ASC, last_tx_lt ASC"). Limit(limit). Scan(ctx, &ids) if err != nil { diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 5cd454ae..c5b04588 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -290,7 +290,7 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, q := r.ch.NewSelect().Model((*core.AccountState)(nil)). ColumnExpr("address"). - Where("contract_name = ?", contractName) + Where("contract_name = ?", string(contractName)) if afterAddress != nil { q = q.Where("address >= ?", afterAddress) } @@ -316,7 +316,7 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, q = q.Where(fmt.Sprintf("(%s, %s) > (?, ?)", addrCol, ltCol), afterAddress, afterTxLt) } err = q. - Order(fmt.Sprintf("%s ASC, %s ASC", addrCol, ltCol)). + OrderExpr(fmt.Sprintf("%s ASC, %s ASC", addrCol, ltCol)). Limit(limit). Scan(ctx, &hashes) if err != nil { From 82310dfce5e872bbc03011a0d85ecd40047e78d9 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 14:33:58 +0700 Subject: [PATCH 135/186] [repo] rescan: fix GetUnfinishedRescanTask typo, close transaction on SetRescanTask error --- internal/core/repository/rescan/rescan.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/rescan/rescan.go b/internal/core/repository/rescan/rescan.go index 38c0958b..575672c5 100644 --- a/internal/core/repository/rescan/rescan.go +++ b/internal/core/repository/rescan/rescan.go @@ -88,7 +88,7 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core task.Operation = new(core.ContractOperation) err := r.pg.NewSelect().Model(task.Operation). Where("contract_name = ?", task.ContractName). - Where("outgoing IS ?", task.Operation). + Where("outgoing IS ?", task.Outgoing). Where("message_type = ?", task.MessageType). Where("operation_id = ?", task.OperationID). Scan(ctx) @@ -104,6 +104,8 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core } func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.RescanTask) error { + defer func() { _ = tx.Rollback() }() + _, err := tx.NewUpdate().Model(task). Set("finished = ?finished"). Set("last_address = ?last_address"). From f4de29ccf77ad2a54a6572c58b2b6b16b7383e07 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 14:34:42 +0700 Subject: [PATCH 136/186] [core] rescan_tasks table: nullable columns --- internal/core/rescan.go | 8 ++++---- .../pgmigrations/20240213085742_reindex_state.up.sql | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/core/rescan.go b/internal/core/rescan.go index 6a13a543..7111e448 100644 --- a/internal/core/rescan.go +++ b/internal/core/rescan.go @@ -60,12 +60,12 @@ type RescanTask struct { Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` // for get-method update - ChangedGetMethod string `json:"changed_get_method,omitempty"` + ChangedGetMethod string `bun:",nullzero" json:"changed_get_method,omitempty"` // for operations - MessageType MessageType `json:"message_type,omitempty"` - Outgoing bool `json:"outgoing,omitempty"` // if operation is going from contract - OperationID uint32 `json:"operation_id,omitempty"` + MessageType MessageType `bun:"type:message_type,nullzero" json:"message_type,omitempty"` + Outgoing bool `bun:",nullzero" json:"outgoing,omitempty"` // if operation is going from contract + OperationID uint32 `bun:",nullzero" json:"operation_id,omitempty"` Operation *ContractOperation `bun:"rel:has-one,join:contract_name=contract_name,join:outgoing=outgoing,join:operation_id=operation_id" json:"contract_operation"` // checkpoint diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql index 4ef37a60..57862f8c 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.up.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -21,14 +21,14 @@ BEGIN; contract_name text NOT NULL, - changed_get_method text NOT NULL, + changed_get_method text, - message_type message_type NOT NULL, - outgoing boolean NOT NULL, - operation_id integer NOT NULL, + message_type message_type, + outgoing boolean, + operation_id integer, - last_address bytea NOT NULL, - last_tx_lt bigint NOT NULL, + last_address bytea, + last_tx_lt bigint, CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) ); From fcb6f5a4b0dbf4cffd839081a6d49449dd62ebc0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 16:40:24 +0700 Subject: [PATCH 137/186] [rescan] prettify workers start, fix some typos on task finishing --- internal/app/rescan/account.go | 65 ------------------ internal/app/rescan/rescan.go | 116 +++++++++++++++++++++++++++++++++ internal/app/rescan/tx.go | 70 -------------------- 3 files changed, 116 insertions(+), 135 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index c17c674e..7601214b 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -1,10 +1,8 @@ package rescan import ( - "bytes" "context" "reflect" - "sync" "time" "github.com/pkg/errors" @@ -250,66 +248,3 @@ func (s *Service) rescanAccountsWorker(ctx context.Context, task *core.RescanTas return updates } - -func (s *Service) rescanAccounts(ctx context.Context, task *core.RescanTask, ids []*core.AccountStateID) error { - var ( - lastScanned core.AccountStateID - updatesAll []*core.AccountState - updatesChan = make(chan []*core.AccountState) - scanWG sync.WaitGroup - ) - - accRet, err := s.AccountRepo.FilterAccounts(ctx, &filter.AccountsReq{StateIDs: ids}) - if err != nil { - return errors.Wrapf(err, "filter accounts") - } - accounts := accRet.Rows - - workers := s.Workers - if len(accounts) < workers { - workers = len(accounts) - } - - for i := 0; i < len(accounts); { - batchLen := (len(accounts) - i) / workers - if (len(accounts)-i)%workers != 0 { - batchLen++ - } - - scanWG.Add(1) - go func(batch []*core.AccountState) { - defer scanWG.Done() - updatesChan <- s.rescanAccountsWorker(ctx, task, batch) - }(accounts[i : i+batchLen]) - - i += batchLen - workers-- - } - - go func() { - scanWG.Wait() - close(updatesChan) - }() - - for upd := range updatesChan { - for _, upd := range upd { - updID := core.AccountStateID{ - Address: upd.Address, - LastTxLT: upd.LastTxLT, - } - if bytes.Compare(lastScanned.Address[:], updID.Address[:]) >= 0 && lastScanned.LastTxLT >= updID.LastTxLT { - lastScanned = updID - } - } - updatesAll = append(updatesAll, upd...) - } - - if err := s.AccountRepo.UpdateAccountStates(ctx, updatesAll); err != nil { - return errors.Wrapf(err, "update account states") - } - - task.LastAddress = &lastScanned.Address - task.LastTxLt = lastScanned.LastTxLT - - return nil -} diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 423451e4..b9d393ab 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -1,6 +1,7 @@ package rescan import ( + "bytes" "context" "sync" "time" @@ -11,6 +12,7 @@ import ( "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/internal/core/filter" ) var _ app.RescanService = (*Service)(nil) @@ -148,6 +150,10 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro if err != nil { return errors.Wrapf(err, "match states by interface description") } + if len(ids) == 0 { + task.Finished = true + return nil + } if err := s.rescanAccounts(ctx, task, ids); err != nil { return errors.Wrapf(err, "rescan accounts") @@ -160,11 +166,121 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro if err != nil { return errors.Wrapf(err, "get addresses by contract name") } + if len(hashes) == 0 { + task.Finished = true + return nil + } if err := s.rescanMessages(ctx, task, hashes); err != nil { return errors.Wrapf(err, "rescan messages") } + + return nil } return errors.Wrapf(core.ErrInvalidArg, "unknown rescan task type %s", task.Type) } + +func (s *Service) rescanAccounts(ctx context.Context, task *core.RescanTask, ids []*core.AccountStateID) error { + accRet, err := s.AccountRepo.FilterAccounts(ctx, &filter.AccountsReq{StateIDs: ids}) + if err != nil { + return errors.Wrapf(err, "filter accounts") + } + + updates, lastScanned := rescanStartWorkers( + ctx, task, accRet.Rows, + func(v *core.AccountState) core.AccountStateID { + return core.AccountStateID{Address: v.Address, LastTxLT: v.LastTxLT} + }, + s.rescanAccountsWorker, s.Workers) + + if len(updates) > 0 { + if err := s.AccountRepo.UpdateAccountStates(ctx, updates); err != nil { + return errors.Wrapf(err, "update account states") + } + } + + task.LastAddress = &lastScanned.Address + task.LastTxLt = lastScanned.LastTxLT + + return nil +} + +func (s *Service) rescanMessages(ctx context.Context, task *core.RescanTask, hashes [][]byte) error { + messages, err := s.MessageRepo.GetMessages(ctx, hashes) + if err != nil { + return err + } + + updates, lastScanned := rescanStartWorkers( + ctx, task, messages, + func(v *core.Message) core.AccountStateID { + msgID := core.AccountStateID{Address: v.DstAddress, LastTxLT: v.DstTxLT} + if task.Outgoing { + msgID = core.AccountStateID{Address: v.SrcAddress, LastTxLT: v.SrcTxLT} + } + return msgID + }, + s.rescanMessagesWorker, s.Workers) + + if len(updates) > 0 { + if err := s.MessageRepo.UpdateMessages(context.Background(), updates); err != nil { + return errors.Wrap(err, "update messages") + } + } + + task.LastAddress = &lastScanned.Address + task.LastTxLt = lastScanned.LastTxLT + + return nil +} + +func rescanStartWorkers[V any](ctx context.Context, + task *core.RescanTask, + slice []V, + getID func(V) core.AccountStateID, + workerFunc func(context.Context, *core.RescanTask, []V) []V, + workers int, +) (updatesAll []V, lastParsed core.AccountStateID) { + var ( + updatesChan = make(chan []V) + scanWG sync.WaitGroup + ) + + if len(slice) < workers { + workers = len(slice) + } + + for i := 0; i < len(slice); { + batchLen := (len(slice) - i) / workers + if (len(slice)-i)%workers != 0 { + batchLen++ + } + + scanWG.Add(1) + go func(batch []V) { + defer scanWG.Done() + updatesChan <- workerFunc(ctx, task, batch) + }(slice[i : i+batchLen]) + + i += batchLen + workers-- + } + + go func() { + scanWG.Wait() + close(updatesChan) + }() + + for updates := range updatesChan { + for _, upd := range updates { + updID := getID(upd) + if bytes.Compare(lastParsed.Address[:], updID.Address[:]) <= 0 || lastParsed.LastTxLT <= updID.LastTxLT { + lastParsed = updID + } + } + updatesAll = append(updatesAll, updates...) + } + + return updatesAll, lastParsed +} diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 15848df3..af8a88fc 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -1,10 +1,8 @@ package rescan import ( - "bytes" "context" "reflect" - "sync" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -115,71 +113,3 @@ func (s *Service) rescanMessagesWorker(ctx context.Context, task *core.RescanTas return updates } - -func (s *Service) rescanMessages(ctx context.Context, task *core.RescanTask, hashes [][]byte) error { - var ( - lastParsed core.AccountStateID - updatesAll []*core.Message - updatesChan = make(chan []*core.Message) - scanWG sync.WaitGroup - ) - - messages, err := s.MessageRepo.GetMessages(ctx, hashes) - if err != nil { - return err - } - - workers := s.Workers - if len(messages) < workers { - workers = len(messages) - } - - for i := 0; i < len(messages); { - batchLen := (len(messages) - i) / workers - if (len(messages)-i)%workers != 0 { - batchLen++ - } - - scanWG.Add(1) - go func(batch []*core.Message) { - defer scanWG.Done() - updatesChan <- s.rescanMessagesWorker(ctx, task, batch) - }(messages[i : i+batchLen]) - - i += batchLen - workers-- - } - - go func() { - scanWG.Wait() - close(updatesChan) - }() - - for upd := range updatesChan { - for _, upd := range upd { - updID := core.AccountStateID{ - Address: upd.DstAddress, - LastTxLT: upd.DstTxLT, - } - if task.Outgoing { - updID = core.AccountStateID{ - Address: upd.SrcAddress, - LastTxLT: upd.SrcTxLT, - } - } - if bytes.Compare(lastParsed.Address[:], updID.Address[:]) >= 0 && lastParsed.LastTxLT >= updID.LastTxLT { - lastParsed = updID - } - } - updatesAll = append(updatesAll, upd...) - } - - if err := s.MessageRepo.UpdateMessages(context.Background(), updatesAll); err != nil { - return errors.Wrap(err, "update messages") - } - - task.LastAddress = &lastParsed.Address - task.LastTxLt = lastParsed.LastTxLT - - return nil -} From 6b8b387efd344e823e129c4ae67a46acf7be40e4 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 16:43:00 +0700 Subject: [PATCH 138/186] [repo] account: fix MatchStatesByInterfaceDesc --- internal/core/repository/account/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index c1886688..7c7e7632 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -299,7 +299,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, ColumnExpr("last_tx_lt"). WhereGroup(" AND ", func(q *ch.SelectQuery) *ch.SelectQuery { if contractName != "" { - q = q.WhereOr("contract_name = ?", contractName) + q = q.WhereOr("hasAny(types, [?])", contractName) } if len(addresses) > 0 { q = q.WhereOr("address IN (?)", addresses) @@ -314,7 +314,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, return q }) if afterAddress != nil && afterTxLt != 0 { - q = q.Where("(address, after_tx_lt) > (?, ?)", afterAddress, afterTxLt) + q = q.Where("(address, last_tx_lt) > (?, ?)", afterAddress, afterTxLt) } err := q. OrderExpr("address ASC, last_tx_lt ASC"). From e3063f8b9dbaa5a6c328b2e45a1a78e9c52c934c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 16:44:49 +0700 Subject: [PATCH 139/186] [repo] message: fix MatchMessagesByOperationDesc repo --- internal/core/repository/msg/msg.go | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index c5b04588..87a0590d 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -285,32 +285,42 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, afterAddress *addr.Address, afterTxLt uint64, limit int, -) (hashes [][]byte, err error) { - var addresses []*addr.Address +) ([][]byte, error) { + var addressesRet []struct { + Address *addr.Address `ch:"type:String"` + } q := r.ch.NewSelect().Model((*core.AccountState)(nil)). ColumnExpr("address"). - Where("contract_name = ?", string(contractName)) + Where("hasAny(types, [?])", string(contractName)) if afterAddress != nil { q = q.Where("address >= ?", afterAddress) } - err = q. + err := q. Order("address ASC"). Limit(limit). - Scan(ctx, &addresses) + Scan(ctx, &addressesRet) if err != nil { return nil, errors.Wrap(err, "get contract addresses") } + var addresses []*addr.Address + for _, row := range addressesRet { + addresses = append(addresses, row.Address) + } + addrCol, ltCol := "dst_address", "dst_tx_lt" if outgoing { addrCol, ltCol = "src_address", "src_tx_lt" } + var msgHashesRet []struct { + Hash []byte + } q = r.ch.NewSelect().Model((*core.Message)(nil)). ColumnExpr("hash"). - Where("type = ?", msgType). - Where(addrCol+" IN (?)", bun.In(addresses)). + Where("type = ?", string(msgType)). + Where(addrCol+" IN (?)", ch.In(addresses)). Where("operation_id = ?", operationId) if afterAddress != nil && afterTxLt != 0 { q = q.Where(fmt.Sprintf("(%s, %s) > (?, ?)", addrCol, ltCol), afterAddress, afterTxLt) @@ -318,10 +328,14 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, err = q. OrderExpr(fmt.Sprintf("%s ASC, %s ASC", addrCol, ltCol)). Limit(limit). - Scan(ctx, &hashes) + Scan(ctx, &msgHashesRet) if err != nil { return nil, errors.Wrap(err, "get message hashes") } + var hashes [][]byte + for _, row := range msgHashesRet { + hashes = append(hashes, row.Hash) + } return hashes, nil } From 9b7523415a4a87a3c70aee3172427310d80b1f86 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 17:44:24 +0700 Subject: [PATCH 140/186] [repo] account: fix MatchStatesByInterfaceDesc filter by contract --- internal/core/repository/account/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 7c7e7632..371e5bed 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -299,7 +299,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, ColumnExpr("last_tx_lt"). WhereGroup(" AND ", func(q *ch.SelectQuery) *ch.SelectQuery { if contractName != "" { - q = q.WhereOr("hasAny(types, [?])", contractName) + q = q.WhereOr("hasAny(types, [?])", string(contractName)) } if len(addresses) > 0 { q = q.WhereOr("address IN (?)", addresses) From 0376a52b7f0639f466bcd419ef34c939a8a66977 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 17:45:05 +0700 Subject: [PATCH 141/186] [repo] rescan: do not select interface relation on GetUnfinishedRescanTask --- internal/core/repository/rescan/rescan.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/core/repository/rescan/rescan.go b/internal/core/repository/rescan/rescan.go index 575672c5..6f31218f 100644 --- a/internal/core/repository/rescan/rescan.go +++ b/internal/core/repository/rescan/rescan.go @@ -71,12 +71,13 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core return bun.Tx{}, nil, err } - if task.Type != core.DelInterface { + if task.Type != core.DelInterface && task.Type != core.DelOperation { task.Contract = new(core.ContractInterface) err := r.pg.NewSelect().Model(task.Contract). Where("name = ?", task.ContractName). Scan(ctx) if err != nil { + _ = tx.Rollback() if errors.Is(err, sql.ErrNoRows) { err = core.ErrNotFound } @@ -93,6 +94,7 @@ func (r *Repository) GetUnfinishedRescanTask(ctx context.Context) (bun.Tx, *core Where("operation_id = ?", task.OperationID). Scan(ctx) if err != nil { + _ = tx.Rollback() if errors.Is(err, sql.ErrNoRows) { err = core.ErrNotFound } From 36937dd3ed8a0d359f9c64e696186dc693d4f4aa Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 13 Mar 2024 17:45:35 +0700 Subject: [PATCH 142/186] [rescan] rescanLoop: skip fix not found error skip --- internal/app/rescan/rescan.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index b9d393ab..1b1cea6b 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -3,6 +3,7 @@ package rescan import ( "bytes" "context" + "strings" "sync" "time" @@ -78,7 +79,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) { + 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) From 0b06bfebd472036666204ba59ef1c88b592b7bad Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 15:21:14 +0700 Subject: [PATCH 143/186] [core] rescan task: add timestamps --- internal/core/repository/rescan/rescan.go | 3 +++ internal/core/rescan.go | 4 ++++ migrations/pgmigrations/20240213085742_reindex_state.up.sql | 3 +++ 3 files changed, 10 insertions(+) diff --git a/internal/core/repository/rescan/rescan.go b/internal/core/repository/rescan/rescan.go index 6f31218f..7a46544d 100644 --- a/internal/core/repository/rescan/rescan.go +++ b/internal/core/repository/rescan/rescan.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "strings" + "time" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -42,6 +43,7 @@ func CreateTables(ctx context.Context, pgDB *bun.DB) error { func (r *Repository) AddRescanTask(ctx context.Context, task *core.RescanTask) error { task.ID = 0 + task.CreatedAt = time.Now() _, err := r.pg.NewInsert().Model(task).Exec(ctx) if err != nil { return err @@ -112,6 +114,7 @@ func (r *Repository) SetRescanTask(ctx context.Context, tx bun.Tx, task *core.Re Set("finished = ?finished"). Set("last_address = ?last_address"). Set("last_tx_lt = ?last_tx_lt"). + Set("updated_at = ?", time.Now()). WherePK(). Exec(ctx) if err != nil { diff --git a/internal/core/rescan.go b/internal/core/rescan.go index 7111e448..9dc0cafc 100644 --- a/internal/core/rescan.go +++ b/internal/core/rescan.go @@ -2,6 +2,7 @@ package core import ( "context" + "time" "github.com/uptrace/bun" @@ -71,6 +72,9 @@ type RescanTask struct { // checkpoint LastAddress *addr.Address `bun:"type:bytea" json:"last_address"` LastTxLt uint64 `bun:"type:bigint" json:"last_tx_lt"` + + UpdatedAt time.Time `bun:"type:timestamp without time zone,notnull" json:"updated_at"` + CreatedAt time.Time `bun:"type:timestamp without time zone,notnull" json:"created_at"` } type RescanRepository interface { diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql index 57862f8c..e1c433c6 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.up.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -30,6 +30,9 @@ BEGIN; last_address bytea, last_tx_lt bigint, + updated_at timestamp without time zone NOT NULL, + created_at timestamp without time zone NOT NULL, + CONSTRAINT rescan_tasks_pkey PRIMARY KEY (id) ); From c11c34dd5ee372a74128e5eecca858db16b5a90e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 19:32:07 +0700 Subject: [PATCH 144/186] [repo] select distinct message hashes and account addresses for rescan --- internal/core/repository/account/account.go | 3 +-- internal/core/repository/msg/msg.go | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 371e5bed..f8fbbcb9 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -295,8 +295,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, var ids []*core.AccountStateID q := r.ch.NewSelect().Model((*core.AccountState)(nil)). - ColumnExpr("address"). - ColumnExpr("last_tx_lt"). + ColumnExpr("DISTINCT address, last_tx_lt"). WhereGroup(" AND ", func(q *ch.SelectQuery) *ch.SelectQuery { if contractName != "" { q = q.WhereOr("hasAny(types, [?])", string(contractName)) diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 87a0590d..361aafae 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -291,7 +291,7 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, } q := r.ch.NewSelect().Model((*core.AccountState)(nil)). - ColumnExpr("address"). + ColumnExpr("DISTINCT address"). Where("hasAny(types, [?])", string(contractName)) if afterAddress != nil { q = q.Where("address >= ?", afterAddress) @@ -318,7 +318,7 @@ func (r *Repository) MatchMessagesByOperationDesc(ctx context.Context, Hash []byte } q = r.ch.NewSelect().Model((*core.Message)(nil)). - ColumnExpr("hash"). + ColumnExpr("DISTINCT hash"). Where("type = ?", string(msgType)). Where(addrCol+" IN (?)", ch.In(addresses)). Where("operation_id = ?", operationId) From 8ef13dddd14337e6c574ce6ae0889baeaf394f63 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 19:32:33 +0700 Subject: [PATCH 145/186] [rescan] parseAccountData: fix skip with no interfaces --- internal/app/rescan/account.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 7601214b..069afe2e 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -97,7 +97,7 @@ func (s *Service) clearParsedAccountsData(task *core.RescanTask, acc *core.Accou } func (s *Service) parseAccountData(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { - if known.IsOnlyWalletInterfaces(acc.Types) { + if len(acc.Types) > 0 && known.IsOnlyWalletInterfaces(acc.Types) { // we do not want to emulate wallet get-methods once again, // as there are lots of them, so it takes a lot of CPU usage return @@ -239,7 +239,7 @@ func (s *Service) rescanAccountsWorker(ctx context.Context, task *core.RescanTas s.rescanGetMethod(ctx, task, update) } - if reflect.DeepEqual(acc, &update) { + if reflect.DeepEqual(acc, update) { continue } From b209f5cad8ade08d0a572a3d4b99179b0c4d63dc Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 19:33:25 +0700 Subject: [PATCH 146/186] [parser] callGetMethod: remove previous get-method execution --- internal/app/parser/get.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index bfe9dd0a..28ea2da2 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -105,6 +105,26 @@ func (s *Service) emulateGetMethodNoArgs(ctx context.Context, i *core.ContractIn return stack, nil } +func removeGetMethodExecution(acc *core.AccountState, contract abi.ContractName, gm string) bool { + for it := range acc.ExecutedGetMethods[contract] { + if acc.ExecutedGetMethods[contract][it].Name != gm { + continue + } + executions := acc.ExecutedGetMethods[contract] + copy(executions[it:], executions[it+1:]) + acc.ExecutedGetMethods[contract] = executions[:len(executions)-1] + return true + } + return false +} + +func appendGetMethodExecution(acc *core.AccountState, contract abi.ContractName, exec abi.GetMethodExecution) { + // that's needed to clear array from duplicates (bug is already fixed) + for removeGetMethodExecution(acc, contract, exec.Name) { + } + acc.ExecutedGetMethods[contract] = append(acc.ExecutedGetMethods[contract], exec) +} + func mapContentDataNFT(ret *core.AccountState, c any) { if c == nil { return @@ -156,7 +176,7 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun exec.Address = &collection.Address - acc.ExecutedGetMethods[known.NFTCollection] = append(acc.ExecutedGetMethods[known.NFTCollection], exec) + appendGetMethodExecution(acc, known.NFTCollection, exec) if exec.Error != "" { return } @@ -175,7 +195,7 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta exec.Address = &minter.Address - item.ExecutedGetMethods[i] = append(item.ExecutedGetMethods[i], exec) + appendGetMethodExecution(item, i, exec) if exec.Error != "" { log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) return @@ -239,7 +259,7 @@ func (s *Service) callGetMethod( return errors.Wrapf(err, "execute get-method") } - acc.ExecutedGetMethods[i.Name] = append(acc.ExecutedGetMethods[i.Name], exec) + appendGetMethodExecution(acc, i.Name, exec) if exec.Error != "" { return nil } From 3822db53af39b091878907df1ecf85339c6b2b5c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 20:07:11 +0700 Subject: [PATCH 147/186] [parser] appendGetMethodExecution: fix hugeParam lint --- internal/app/parser/get.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 28ea2da2..ab99c0ef 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -118,11 +118,11 @@ func removeGetMethodExecution(acc *core.AccountState, contract abi.ContractName, return false } -func appendGetMethodExecution(acc *core.AccountState, contract abi.ContractName, exec abi.GetMethodExecution) { +func appendGetMethodExecution(acc *core.AccountState, contract abi.ContractName, exec *abi.GetMethodExecution) { // that's needed to clear array from duplicates (bug is already fixed) for removeGetMethodExecution(acc, contract, exec.Name) { } - acc.ExecutedGetMethods[contract] = append(acc.ExecutedGetMethods[contract], exec) + acc.ExecutedGetMethods[contract] = append(acc.ExecutedGetMethods[contract], *exec) } func mapContentDataNFT(ret *core.AccountState, c any) { @@ -176,7 +176,7 @@ func (s *Service) getNFTItemContent(ctx context.Context, collection *core.Accoun exec.Address = &collection.Address - appendGetMethodExecution(acc, known.NFTCollection, exec) + appendGetMethodExecution(acc, known.NFTCollection, &exec) if exec.Error != "" { return } @@ -195,7 +195,7 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta exec.Address = &minter.Address - appendGetMethodExecution(item, i, exec) + appendGetMethodExecution(item, i, &exec) if exec.Error != "" { log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) return @@ -259,7 +259,7 @@ func (s *Service) callGetMethod( return errors.Wrapf(err, "execute get-method") } - appendGetMethodExecution(acc, i.Name, exec) + appendGetMethodExecution(acc, i.Name, &exec) if exec.Error != "" { return nil } From 4540e449accf80a42c5425cb257734f2c93b08b8 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 21:43:53 +0700 Subject: [PATCH 148/186] [core] rescan tasks: multiple get-methods --- cmd/contract/interface.go | 71 ++++++++++++------- internal/app/rescan/account.go | 30 ++++---- internal/core/rescan.go | 2 +- .../20240213085742_reindex_state.up.sql | 2 +- 4 files changed, 64 insertions(+), 41 deletions(-) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index 725b85f9..f5c146db 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -246,21 +246,37 @@ func diffOperations(oldOperations, newOperations []*core.ContractOperation) (add return diffSlices(oldOperations, newOperations, func(v *core.ContractOperation) string { return v.OperationName }) } -func rescanGetMethod(ctx context.Context, in abi.ContractName, repo core.RescanRepository, t core.RescanTaskType, gm string) error { +func getGetMethodNames(desc []abi.GetMethodDesc) (names []string) { + for i := range desc { + if len(desc[i].Arguments) > 0 { + continue + } + names = append(names, desc[i].Name) + } + return +} + +func rescanGetMethod(ctx context.Context, in abi.ContractName, repo core.RescanRepository, t core.RescanTaskType, getMethods []string) error { + if len(getMethods) == 0 { + return nil + } + err := repo.AddRescanTask(ctx, &core.RescanTask{ - Type: t, - ContractName: in, - ChangedGetMethod: gm, + Type: t, + ContractName: in, + ChangedGetMethods: getMethods, }) if err != nil { - return errors.Wrapf(err, "add rescan task for '%s' get-method", gm) + return errors.Wrapf(err, "add rescan task for '%s' get-method", getMethods) } - log.Info(). - Str("rescan_type", string(t)). - Str("interface_name", string(in)). - Str("get_method", gm). - Msg("added get-method rescan task") + for _, gm := range getMethods { + log.Info(). + Str("rescan_type", string(t)). + Str("interface_name", string(in)). + Str("get_method", gm). + Msg("added get-method rescan task") + } return nil } @@ -334,11 +350,21 @@ var Command = &cli.Command{ contractRepo := contract.NewRepository(pg) rescanRepo := rescan.NewRepository(pg) - for dn, d := range definitions { + addedDef, changedDef, err := diffDefinitions(ctx.Context, contractRepo, definitions) + if err != nil { + return err + } + for dn, d := range changedDef { + if err := contractRepo.UpdateDefinition(ctx.Context, dn, d); err != nil { + return errors.Wrapf(err, "cannot update contract definition '%s'", dn) + } + } + for dn, d := range addedDef { if err := contractRepo.AddDefinition(ctx.Context, dn, d); err != nil { - log.Error().Err(err).Str("definition_name", string(dn)).Msg("cannot insert contract definition") + return errors.Wrapf(err, "cannot insert contract definition '%s'", dn) } } + for _, i := range interfaces { if err := contractRepo.AddInterface(ctx.Context, i); err != nil { log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot insert contract interface") @@ -352,6 +378,7 @@ var Command = &cli.Command{ log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot add interface rescan task") } } + for _, op := range operations { if err := contractRepo.AddOperation(ctx.Context, op); err != nil { log.Error().Err(err). @@ -486,20 +513,14 @@ var Command = &cli.Command{ } } - for _, gm := range addedGm { - if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.AddGetMethod, gm.Name); err != nil { - return err - } + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.AddGetMethod, getGetMethodNames(addedGm)); err != nil { + return err } - for _, gm := range changedGm { - if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.UpdGetMethod, gm.Name); err != nil { - return err - } + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.UpdGetMethod, getGetMethodNames(changedGm)); err != nil { + return err } - for _, gm := range deletedGm { - if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.DelGetMethod, gm.Name); err != nil { - return err - } + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.DelGetMethod, getGetMethodNames(deletedGm)); err != nil { + return err } for _, op := range deletedOp { @@ -545,7 +566,7 @@ var Command = &cli.Command{ oldInterface, err := contractRepo.GetInterface(ctx.Context, contractName) if err != nil { - return errors.Wrapf(err, "get '%s' interface", oldInterface.Name) + return errors.Wrapf(err, "get '%s' interface", contractName) } if err := contractRepo.DeleteInterface(ctx.Context, contractName); err != nil { diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 069afe2e..1f2b05bd 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -123,14 +123,14 @@ func (s *Service) rescanInterface(ctx context.Context, task *core.RescanTask, ac s.parseAccountData(ctx, task, acc) } -func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.AccountState) { +func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.AccountState, gm string) { _, ok := acc.ExecutedGetMethods[task.ContractName] if !ok { return } for it := range acc.ExecutedGetMethods[task.ContractName] { - if acc.ExecutedGetMethods[task.ContractName][it].Name != task.ChangedGetMethod { + if acc.ExecutedGetMethods[task.ContractName][it].Name != gm { continue } executions := acc.ExecutedGetMethods[task.ContractName] @@ -145,7 +145,7 @@ func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.Accoun return } - switch task.ChangedGetMethod { + switch gm { case "get_nft_content", "get_collection_data", "get_jetton_data": acc.ContentURI = "" acc.ContentName = "" @@ -154,7 +154,7 @@ func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.Accoun acc.ContentImageData = nil } - switch task.ChangedGetMethod { + switch gm { case "get_collection_data": acc.OwnerAddress = nil case "get_nft_data", "get_wallet_data": @@ -165,36 +165,36 @@ func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.Accoun acc.JettonBalance = nil } - switch task.ChangedGetMethod { + switch gm { case "get_wallet_address", "get_nft_address_by_index": acc.Fake = false } } -func (s *Service) executeGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { +func (s *Service) executeGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState, gm string) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { return s.getRecentAccountState(ctx, acc.BlockID(), a) } - err := s.Parser.ExecuteAccountGetMethod(ctx, task.ContractName, task.ChangedGetMethod, acc, getOtherAccountFunc) + err := s.Parser.ExecuteAccountGetMethod(ctx, task.ContractName, gm, acc, getOtherAccountFunc) if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { log.Error().Err(err). Str("contract_name", string(task.ContractName)). - Str("get_method", task.ChangedGetMethod). + Str("get_method", gm). Str("addr", acc.Address.Base64()). Msg("parse account data") } } -func (s *Service) rescanGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState) { - s.clearExecutedGetMethod(task, acc) +func (s *Service) rescanGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState, gm string) { + s.clearExecutedGetMethod(task, acc, gm) matchedByGetMethod := func() (matchedByGM, hasGM bool) { if len(task.Contract.Code) > 0 || len(task.Contract.Addresses) > 0 { return false, false } - changed := abi.MethodNameHash(task.ChangedGetMethod) + changed := abi.MethodNameHash(gm) for _, gmh := range task.Contract.GetMethodHashes { if gmh == changed { return true, true @@ -212,7 +212,7 @@ func (s *Service) rescanGetMethod(ctx context.Context, task *core.RescanTask, ac return } - s.executeGetMethod(ctx, task, acc) + s.executeGetMethod(ctx, task, acc, gm) case core.DelGetMethod: m, h := matchedByGetMethod() @@ -224,7 +224,7 @@ func (s *Service) rescanGetMethod(ctx context.Context, task *core.RescanTask, ac } case core.UpdGetMethod: - s.executeGetMethod(ctx, task, acc) + s.executeGetMethod(ctx, task, acc, gm) } } @@ -236,7 +236,9 @@ func (s *Service) rescanAccountsWorker(ctx context.Context, task *core.RescanTas case core.AddInterface, core.UpdInterface, core.DelInterface: s.rescanInterface(ctx, task, update) case core.AddGetMethod, core.UpdGetMethod, core.DelGetMethod: - s.rescanGetMethod(ctx, task, update) + for _, gm := range task.ChangedGetMethods { + s.rescanGetMethod(ctx, task, update, gm) + } } if reflect.DeepEqual(acc, update) { diff --git a/internal/core/rescan.go b/internal/core/rescan.go index 9dc0cafc..56379d2b 100644 --- a/internal/core/rescan.go +++ b/internal/core/rescan.go @@ -61,7 +61,7 @@ type RescanTask struct { Contract *ContractInterface `bun:"rel:has-one,join:contract_name=name" json:"contract_interface"` // for get-method update - ChangedGetMethod string `bun:",nullzero" json:"changed_get_method,omitempty"` + ChangedGetMethods []string `bun:"type:text[],array" json:"changed_get_methods,omitempty"` // for operations MessageType MessageType `bun:"type:message_type,nullzero" json:"message_type,omitempty"` diff --git a/migrations/pgmigrations/20240213085742_reindex_state.up.sql b/migrations/pgmigrations/20240213085742_reindex_state.up.sql index e1c433c6..414110f0 100644 --- a/migrations/pgmigrations/20240213085742_reindex_state.up.sql +++ b/migrations/pgmigrations/20240213085742_reindex_state.up.sql @@ -21,7 +21,7 @@ BEGIN; contract_name text NOT NULL, - changed_get_method text, + changed_get_methods text[], message_type message_type, outgoing boolean, From a5817631169dabb1044ece6733e9e20cfa593e9a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 21:44:38 +0700 Subject: [PATCH 149/186] [rescan] fix account state id iteration --- internal/app/rescan/rescan.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 1b1cea6b..4d1f9656 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -274,14 +274,15 @@ func rescanStartWorkers[V any](ctx context.Context, }() for updates := range updatesChan { - for _, upd := range updates { - updID := getID(upd) - if bytes.Compare(lastParsed.Address[:], updID.Address[:]) <= 0 || lastParsed.LastTxLT <= updID.LastTxLT { - lastParsed = updID - } - } updatesAll = append(updatesAll, updates...) } + for it := range slice { + vID := getID(slice[it]) + if bytes.Compare(lastParsed.Address[:], vID.Address[:]) <= 0 || lastParsed.LastTxLT <= vID.LastTxLT { + lastParsed = vID + } + } + return updatesAll, lastParsed } From 1d3c0123632dd446ba69a6d79ca8b4f4e848f939 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 22:58:31 +0700 Subject: [PATCH 150/186] [parser] remove hardcoded getters for nft and jetton minter check and content --- internal/app/parser/get.go | 57 ++++++++++---------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index ab99c0ef..9d27a996 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -149,26 +149,15 @@ func mapContentDataNFT(ret *core.AccountState, c any) { } func (s *Service) getNFTItemContent(ctx context.Context, collection *core.AccountState, idx *big.Int, itemContent *cell.Cell, acc *core.AccountState) { - desc := &abi.GetMethodDesc{ - Name: "get_nft_content", - Arguments: []abi.VmValueDesc{{ - Name: "index", - StackType: "int", - Format: "bytes", - }, { - Name: "individual_content", - StackType: "cell", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "full_content", - StackType: "cell", - Format: "content", - }}, + desc, err := s.ContractRepo.GetMethodDescription(ctx, known.NFTCollection, "get_nft_content") + if err != nil { + log.Error().Err(err).Msg("get 'get_nft_content' method description") + return } args := []any{idx.Bytes(), itemContent} - exec, err := s.emulateGetMethod(ctx, desc, collection, args) + exec, err := s.emulateGetMethod(ctx, &desc, collection, args) if err != nil { log.Error().Err(err).Msg("execute get_nft_content nft_collection get-method") return @@ -208,43 +197,27 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta } func (s *Service) checkNFTMinter(ctx context.Context, minter *core.AccountState, idx *big.Int, item *core.AccountState) { - desc := &abi.GetMethodDesc{ - Name: "get_nft_address_by_index", - Arguments: []abi.VmValueDesc{{ - Name: "index", - StackType: "int", - Format: "bytes", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "address", - StackType: "slice", - Format: "addr", - }}, + desc, err := s.ContractRepo.GetMethodDescription(ctx, known.NFTCollection, "get_nft_address_by_index") + if err != nil { + log.Error().Err(err).Msg("get 'get_nft_address_by_index' method description") + return } args := []any{idx.Bytes()} - s.checkMinter(ctx, minter, item, known.NFTCollection, desc, args) + s.checkMinter(ctx, minter, item, known.NFTCollection, &desc, args) } func (s *Service) checkJettonMinter(ctx context.Context, minter *core.AccountState, ownerAddr *addr.Address, walletAcc *core.AccountState) { - desc := &abi.GetMethodDesc{ - Name: "get_wallet_address", - Arguments: []abi.VmValueDesc{{ - Name: "owner_address", - StackType: "slice", - Format: "addr", - }}, - ReturnValues: []abi.VmValueDesc{{ - Name: "wallet_address", - StackType: "slice", - Format: "addr", - }}, + desc, err := s.ContractRepo.GetMethodDescription(ctx, known.JettonMinter, "get_wallet_address") + if err != nil { + log.Error().Err(err).Msg("get 'get_wallet_address' method description") + return } args := []any{ownerAddr.MustToTonutils()} - s.checkMinter(ctx, minter, walletAcc, known.JettonMinter, desc, args) + s.checkMinter(ctx, minter, walletAcc, known.JettonMinter, &desc, args) } func (s *Service) callGetMethod( From c6015c725c88478a0e6acaae1e70b97b57920471 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 14 Mar 2024 22:59:55 +0700 Subject: [PATCH 151/186] [rescan] update UpdInterface task description --- internal/app/parser.go | 5 ++++- internal/app/parser/account.go | 2 +- internal/app/rescan/account.go | 2 +- internal/app/rescan/rescan.go | 4 ++-- internal/core/rescan.go | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/app/parser.go b/internal/app/parser.go index 8890f4b3..d14be3fc 100644 --- a/internal/app/parser.go +++ b/internal/app/parser.go @@ -15,7 +15,10 @@ import ( "github.com/tonindexer/anton/internal/core" ) -var ErrImpossibleParsing = errors.New("parsing is impossible") +var ( + ErrImpossibleParsing = errors.New("parsing is impossible") + ErrUnmatchedContractInterface = errors.New("account state does not match the contract interface description") +) type ParserConfig struct { BlockchainConfig *cell.Cell diff --git a/internal/app/parser/account.go b/internal/app/parser/account.go index b9210eb7..1d495e8b 100644 --- a/internal/app/parser/account.go +++ b/internal/app/parser/account.go @@ -132,7 +132,7 @@ func (s *Service) ParseAccountContractData( others func(context.Context, addr.Address) (*core.AccountState, error), ) error { if !interfaceMatched(acc, contractDesc) { - return errors.Wrap(core.ErrInvalidArg, "account state does not match the contract interface description") + return app.ErrUnmatchedContractInterface } var contractTypeSet bool diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 1f2b05bd..70a09065 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -108,7 +108,7 @@ func (s *Service) parseAccountData(ctx context.Context, task *core.RescanTask, a } err := s.Parser.ParseAccountContractData(ctx, task.Contract, acc, getOtherAccountFunc) - if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { + if err != nil && !errors.Is(err, app.ErrUnmatchedContractInterface) { log.Error().Err(err).Str("addr", acc.Address.Base64()).Msg("parse account data") } } diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 4d1f9656..9d14399f 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -130,7 +130,7 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro return nil - case core.UpdInterface, core.DelInterface: + case core.DelInterface: ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, task.ContractName, nil, nil, nil, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { return errors.Wrapf(err, "match states by interface description") @@ -146,7 +146,7 @@ func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) erro return nil - case core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod: + case core.UpdInterface, core.AddGetMethod, core.DelGetMethod, core.UpdGetMethod: ids, err := s.AccountRepo.MatchStatesByInterfaceDesc(ctx, task.ContractName, task.Contract.Addresses, codeHash, task.Contract.GetMethodHashes, task.LastAddress, task.LastTxLt, s.SelectLimit) if err != nil { return errors.Wrapf(err, "match states by interface description") diff --git a/internal/core/rescan.go b/internal/core/rescan.go index 56379d2b..16d49b78 100644 --- a/internal/core/rescan.go +++ b/internal/core/rescan.go @@ -18,9 +18,9 @@ const ( // execute get methods on these pairs, and update the account states with the newly parsed data. AddInterface RescanTaskType = "add_interface" - // UpdInterface filters the account states by the already set contract name. - // Again, collect (address, last_tx_lt) pairs, execute get methods, - // update the account states with the parsed data. + // UpdInterface is invoked when changes occur to the interface code, addresses, or get-methods. + // This requires removing parsed data from account states that are no longer relevant + // and reparsing data for account states that have become relevant due to the changes. UpdInterface RescanTaskType = "upd_interface" // DelInterface does the same filtering as UpdInterface, From 7923bb83937ae8c26b548aff302fff0eaba64035 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 17:28:12 +0700 Subject: [PATCH 152/186] [repo] account: fix typo in MatchStatesByInterfaceDesc --- internal/core/repository/account/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index f8fbbcb9..22d4e80e 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -301,7 +301,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, q = q.WhereOr("hasAny(types, [?])", string(contractName)) } if len(addresses) > 0 { - q = q.WhereOr("address IN (?)", addresses) + q = q.WhereOr("address IN ?", ch.In(addresses)) } if len(codeHash) > 0 { q = q.WhereOr("code_hash = ?", codeHash) From 6bdca070132d882991bad26693dcfdeb9a414e5d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 17:28:53 +0700 Subject: [PATCH 153/186] [rescan] fix typo in rescanMessage argument --- internal/app/rescan/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index af8a88fc..2b607222 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -101,7 +101,7 @@ func (s *Service) rescanMessagesWorker(ctx context.Context, task *core.RescanTas upd.SrcContract, upd.DstContract, upd.OperationName, upd.DataJSON, upd.Error = "", "", "", nil, "" case core.UpdOperation: - if err := s.rescanMessage(ctx, task, msg); err != nil { + if err := s.rescanMessage(ctx, task, &upd); err != nil { continue } } From 2dd737c7e9978bb6e45ecad6a8d83c94f43cc876 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 17:30:41 +0700 Subject: [PATCH 154/186] [abi] known: remove empty arguments from interfaces, parse nft index as bytes --- abi/known/telemint.json | 3 --- abi/known/tep62_nft.json | 12 +++++------- abi/known/tonpay.json | 1 - 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/abi/known/telemint.json b/abi/known/telemint.json index adec00ad..4439494d 100644 --- a/abi/known/telemint.json +++ b/abi/known/telemint.json @@ -278,7 +278,6 @@ "get_methods": [ { "name": "get_telemint_token_name", - "arguments": [], "return_values": [ { "name": "token_name", @@ -288,7 +287,6 @@ }, { "name": "get_telemint_auction_state", - "arguments": [], "return_values": [ { "name": "bidder_address", @@ -315,7 +313,6 @@ }, { "name": "get_telemint_auction_config", - "arguments": [], "return_values": [ { "name": "beneficiary_address", diff --git a/abi/known/tep62_nft.json b/abi/known/tep62_nft.json index 63188fd2..67c078e5 100644 --- a/abi/known/tep62_nft.json +++ b/abi/known/tep62_nft.json @@ -100,7 +100,6 @@ "get_methods": [ { "name": "get_nft_data", - "arguments": [], "return_values": [ { "name": "init", @@ -109,7 +108,7 @@ }, { "name": "index", - "stack_type": "int", + "stack_type": "int", "format": "bytes" }, { @@ -223,11 +222,11 @@ "get_methods": [ { "name": "get_collection_data", - "arguments": [], "return_values": [ { "name": "next_item_index", - "stack_type": "int" + "stack_type": "int", + "format": "bytes" }, { "name": "collection_content", @@ -246,7 +245,8 @@ "arguments": [ { "name": "index", - "stack_type": "int" + "stack_type": "int", + "format": "bytes" } ], "return_values": [ @@ -321,7 +321,6 @@ "get_methods": [ { "name": "royalty_params", - "arguments": [], "return_values": [ { "name": "numerator", @@ -426,7 +425,6 @@ "get_methods": [ { "name": "get_editor", - "arguments": [], "return_values": [ { "name": "editor", diff --git a/abi/known/tonpay.json b/abi/known/tonpay.json index 9360ca76..7fa604cc 100644 --- a/abi/known/tonpay.json +++ b/abi/known/tonpay.json @@ -471,7 +471,6 @@ "get_methods": [ { "name": "get_invoice_data", - "arguments": [], "return_values": [ { "name": "store", From 4fad89d7087f205f1fa378becd48631247c34646 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 18:46:56 +0700 Subject: [PATCH 155/186] [abi] telemint: fix parsing of get_telemint_token_name --- abi/known/telemint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abi/known/telemint.json b/abi/known/telemint.json index 4439494d..a9ac8f71 100644 --- a/abi/known/telemint.json +++ b/abi/known/telemint.json @@ -281,7 +281,8 @@ "return_values": [ { "name": "token_name", - "stack_type": "slice" + "stack_type": "slice", + "format": "string" } ] }, From 578667791cd282eaff6f90eaf54df8311c0678b3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 18:47:30 +0700 Subject: [PATCH 156/186] [repo] account: sort executed get-methods --- internal/core/repository/account/account.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 22d4e80e..7aa48efb 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "reflect" + "sort" "strings" "github.com/pkg/errors" @@ -200,6 +201,10 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ } for _, a := range accounts { + for _, executions := range a.ExecutedGetMethods { + sort.Slice(executions, func(i, j int) bool { return executions[i].Name < executions[j].Name }) + } + _, err := tx.NewInsert().Model(a).Exec(ctx) if err != nil { return errors.Wrapf(err, "cannot insert new %s acc state", a.Address.String()) @@ -254,6 +259,10 @@ func (r *Repository) UpdateAccountStates(ctx context.Context, accounts []*core.A } for _, a := range accounts { + for _, executions := range a.ExecutedGetMethods { + sort.Slice(executions, func(i, j int) bool { return executions[i].Name < executions[j].Name }) + } + logAccountStateDataUpdate(a) _, err := r.pg.NewUpdate().Model(a). From b82cb93d3f492db2c25c73e9990e2f3ec006166a Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 18:48:13 +0700 Subject: [PATCH 157/186] [cmd] contract: fix some typos, add logs --- cmd/contract/interface.go | 47 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/cmd/contract/interface.go b/cmd/contract/interface.go index f5c146db..a99c2b73 100644 --- a/cmd/contract/interface.go +++ b/cmd/contract/interface.go @@ -195,6 +195,7 @@ func diffDefinitions(ctx context.Context, contractRepo core.ContractRepository, od, ok := old[dt] if !ok { added[dt] = d + continue } if !reflect.DeepEqual(od, d) { changed[dt] = d @@ -217,6 +218,7 @@ func diffSlices[V any](oldS, newS []V, getName func(v V) string) (added, changed ov, ok := oldM[vn] if !ok { added = append(added, v) + continue } if !reflect.DeepEqual(ov, v) { changed = append(changed, v) @@ -256,6 +258,23 @@ func getGetMethodNames(desc []abi.GetMethodDesc) (names []string) { return } +func rescanInterface(ctx context.Context, in abi.ContractName, repo core.RescanRepository, t core.RescanTaskType) error { + err := repo.AddRescanTask(ctx, &core.RescanTask{ + Type: t, + ContractName: in, + }) + if err != nil { + return errors.Wrapf(err, "add rescan task for '%s' contract interface", in) + } + + log.Info(). + Str("rescan_type", string(t)). + Str("interface_name", string(in)). + Msg("added contract interface rescan task") + + return nil +} + func rescanGetMethod(ctx context.Context, in abi.ContractName, repo core.RescanRepository, t core.RescanTaskType, getMethods []string) error { if len(getMethods) == 0 { return nil @@ -370,10 +389,7 @@ var Command = &cli.Command{ log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot insert contract interface") continue } - err := rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ - Type: core.AddInterface, - ContractName: i.Name, - }) + err := rescanInterface(ctx.Context, i.Name, rescanRepo, core.AddInterface) if err != nil { log.Error().Err(err).Str("interface_name", string(i.Name)).Msg("cannot add interface rescan task") } @@ -387,13 +403,7 @@ var Command = &cli.Command{ Msg("cannot insert contract operation") continue } - err := rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ - Type: core.UpdOperation, - ContractName: op.ContractName, - MessageType: op.MessageType, - Outgoing: op.Outgoing, - OperationID: op.OperationID, - }) + err := rescanOperation(ctx.Context, rescanRepo, core.UpdOperation, op) if err != nil { log.Error().Err(err). Str("interface_name", string(op.ContractName)). @@ -490,7 +500,7 @@ var Command = &cli.Command{ } iChanged, addedGm, changedGm, deletedGm := diffInterface(oldInterface, newInterface) - if iChanged { + if iChanged || len(addedGm) > 0 || len(changedGm) > 0 || len(deletedGm) > 0 { if err := contractRepo.UpdateInterface(ctx.Context, newInterface); err != nil { return errors.Wrapf(err, "cannot update contract interface '%s'", newInterface.Name) } @@ -513,6 +523,12 @@ var Command = &cli.Command{ } } + if iChanged { + if err := rescanInterface(ctx.Context, contractName, rescanRepo, core.UpdInterface); err != nil { + return err + } + } + if err := rescanGetMethod(ctx.Context, contractName, rescanRepo, core.AddGetMethod, getGetMethodNames(addedGm)); err != nil { return err } @@ -579,12 +595,9 @@ var Command = &cli.Command{ } } - err = rescanRepo.AddRescanTask(ctx.Context, &core.RescanTask{ - Type: core.DelInterface, - ContractName: contractName, - }) + err = rescanInterface(ctx.Context, contractName, rescanRepo, core.DelInterface) if err != nil { - log.Error().Err(err).Str("interface_name", string(contractName)).Msg("cannot add interface rescan task") + return err } return nil From 7f33eb994e348532c255541e5c9c4a8e0c19acb0 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 18:56:30 +0700 Subject: [PATCH 158/186] [rescan] rescanRunTask: add gocyclo nolint directive --- internal/app/rescan/rescan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 9d14399f..695cd664 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -103,7 +103,7 @@ func (s *Service) rescanLoop() { } } -func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { +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) From 8c75cffe39d1403c143c578eea69489179c57c6f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 19:31:14 +0700 Subject: [PATCH 159/186] README.md: update rescan description --- README.md | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 4b230923..64b8fc15 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ To explore how Anton stores data, visit the [migrations' directory](/migrations) | `app/parser` | service determines contract interfaces, parse contract data and message payloads | | `app/fetcher` | service concurrently fetches data from blockchain | | `app/indexer` | service scans blocks and save parsed data to databases | +| `app/rescan` | service parses data by updated contract description | | `app/query` | service aggregates database repositories | | `api/http` | implements the REST API | @@ -103,18 +104,19 @@ cp .env.example .env nano .env ``` -| Name | Description | Default | Example | -|------------------|-----------------------------------|---------|--------------------------------------------------------------------| -| `DB_NAME` | Database name | | idx | -| `DB_USERNAME` | Database username | | user | -| `DB_PASSWORD` | Database password | | pass | -| `DB_CH_URL` | Clickhouse URL to connect to | | clickhouse://clickhouse:9000/db_name?sslmode=disable | -| `DB_PG_URL` | PostgreSQL URL to connect to | | postgres://username:password@postgres:5432/db_name?sslmode=disable | -| `FROM_BLOCK` | Master chain seq_no to start from | 1 | 23532000 | -| `WORKERS` | Number of indexer workers | 4 | 8 | -| `RESCAN_WORKERS` | Number of rescan workers | 4 | 8 | -| `LITESERVERS` | Lite servers to connect to | | 135.181.177.59:53312 aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= | -| `DEBUG_LOGS` | Debug logs enabled | false | true | +| Name | Description | Default | Example | +|-----------------------|------------------------------------|---------|--------------------------------------------------------------------| +| `DB_NAME` | Database name | | idx | +| `DB_USERNAME` | Database username | | user | +| `DB_PASSWORD` | Database password | | pass | +| `DB_CH_URL` | Clickhouse URL to connect to | | clickhouse://clickhouse:9000/db_name?sslmode=disable | +| `DB_PG_URL` | PostgreSQL URL to connect to | | postgres://username:password@postgres:5432/db_name?sslmode=disable | +| `FROM_BLOCK` | Master chain seq_no to start from | 1 | 23532000 | +| `WORKERS` | Number of indexer workers | 4 | 8 | +| `RESCAN_WORKERS` | Number of rescan workers | 4 | 8 | +| `RESCAN_SELECT_LIMIT` | Number of rows to fetch for rescan | 3000 | 1000 | +| `LITESERVERS` | Lite servers to connect to | | 135.181.177.59:53312 aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ= | +| `DEBUG_LOGS` | Debug logs enabled | false | true | ### Building @@ -158,7 +160,7 @@ To run Anton, you need at least one defined contract interface. There are some known interfaces in the [abi/known](/abi/known) directory. You can add them through this command: ```shell -docker compose exec web sh -c "anton contract /var/anton/known/*.json" +docker compose exec rescan sh -c "anton contract addInterfaces /var/anton/known/*.json" ``` ### Database schema migration @@ -218,38 +220,44 @@ docker run tonindexer/anton archive [--testnet] ### Inserting contract interface +To add interfaces, you need to provide Anton with a contract description. +It will select any interfaces not already present in the database, +insert them, and initiate rescan tasks for messages and account states. + ```shell # add from stdin -cat abi/known/tep81_dns.json | docker compose exec -T web anton contract --stdin +cat abi/known/tep81_dns.json | docker compose exec -T web anton contract addInterfaces --stdin # add from file -docker compose exec web anton contract "/var/anton/known/tep81_dns.json" +docker compose exec web anton contract addInterfaces "/var/anton/known/tep81_dns.json" ``` ### Deleting contract interface -```shell -docker compose exec web anton contract delete "dns_nft_item" -``` - -### Addding address label +To delete an interface, provide a contract description along with the specific contract name you wish to remove. +Anton will then delete the contract interface and its associated operations from the database +and initiate rescan tasks to remove all parsed data related to this interface from messages and account states. ```shell -docker compose exec web anton label "EQDj5AA8mQvM5wJEQsFFFof79y3ZsuX6wowktWQFhz_Anton" "anton.tools" - -# known tonscan labels -docker compose exec web anton label --tonscan +docker compose exec rescan sh -c "anton contract deleteInterface -c nft_item /var/anton/known/*.json" ``` -## Rescanning +### Updating contract interface -If you've modified a contract interface by adding new ones or deleting old ones, -you can reparse account states and messages. +To update a contract interface, you need to provide both the contract description +and the specific name of the contract you're updating. +Anton will then compare the provided contract interface description against the existing interface in the database. +If there are any differences, Anton initiates rescan tasks to reparse data and fix these changes. +This process may involve adding, deleting, or updating get-methods and contract operations. -To accomplish this, create a task specifying the masterchain block number from which to start rescanning. -The rescan service will then iterate through the database data and update rows based on the new contract descriptions. +```shell +docker compose exec rescan sh -c "anton contract updateInterface -c telemint_nft_item /var/anton/known/telemint.json" +``` -### Adding a rescan task +### Adding address label ```shell -docker compose exec web anton contract rescan 24400000 +docker compose exec web anton label "EQDj5AA8mQvM5wJEQsFFFof79y3ZsuX6wowktWQFhz_Anton" "anton.tools" + +# known tonscan labels +docker compose exec web anton label --tonscan ``` From 1813c3c532453e6b9504234a1b9eb351be494492 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 19:46:48 +0700 Subject: [PATCH 160/186] [repo] account.filterAccountStates: add nolint directive for gocognit --- 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 0b8baf13..0d6ea1f9 100644 --- a/internal/core/repository/account/filter.go +++ b/internal/core/repository/account/filter.go @@ -83,7 +83,7 @@ func flattenStateIDs(ids []*core.AccountStateID) (ret [][]any) { return } -func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { //nolint:gocyclo // that's ok +func (r *Repository) filterAccountStates(ctx context.Context, f *filter.AccountsReq, total int) (ret []*core.AccountState, err error) { //nolint:gocyclo,gocognit // that's ok var ( q *bun.SelectQuery prefix, statesTable string From 540b5d8c973e75c0e255925f3fbb3209938fc3dd Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 19:47:05 +0700 Subject: [PATCH 161/186] [rescan] rescanRunTask: add nolint directive for gocognit --- internal/app/rescan/rescan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index 695cd664..cf8e61cd 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -103,7 +103,7 @@ func (s *Service) rescanLoop() { } } -func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { //nolint:gocyclo // yeah, it's a bit long +func (s *Service) rescanRunTask(ctx context.Context, task *core.RescanTask) error { //nolint:gocyclo,gocognit // yeah, it's a bit long var codeHash []byte if task.Contract != nil && task.Contract.Code != nil { codeCell, err := cell.FromBOC(task.Contract.Code) From f281b3e04e7c48bd49193893f836eeffa4042366 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 19:55:49 +0700 Subject: [PATCH 162/186] [parser] fix tests --- internal/app/parser/parser_test.go | 56 ++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/internal/app/parser/parser_test.go b/internal/app/parser/parser_test.go index 576a625b..2ad3291a 100644 --- a/internal/app/parser/parser_test.go +++ b/internal/app/parser/parser_test.go @@ -3,12 +3,14 @@ package parser import ( "context" "encoding/base64" + "fmt" "testing" "github.com/stretchr/testify/require" "github.com/xssnick/tonutils-go/tvm/cell" "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/abi/known" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" ) @@ -62,8 +64,58 @@ func (m *mockContractRepo) GetInterfaces(_ context.Context) ([]*core.ContractInt func (m *mockContractRepo) GetInterface(context.Context, abi.ContractName) (*core.ContractInterface, error) { panic("implement me") } -func (m *mockContractRepo) GetMethodDescription(context.Context, abi.ContractName, string) (abi.GetMethodDesc, error) { - panic("implement me") +func (m *mockContractRepo) GetMethodDescription(_ context.Context, contract abi.ContractName, gm string) (abi.GetMethodDesc, error) { + switch { + case contract == known.NFTCollection && gm == "get_nft_content": + return abi.GetMethodDesc{ + Name: "get_nft_content", + Arguments: []abi.VmValueDesc{{ + Name: "index", + StackType: "int", + Format: "bytes", + }, { + Name: "individual_content", + StackType: "cell", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "full_content", + StackType: "cell", + Format: "content", + }}, + }, nil + + case contract == known.NFTCollection && gm == "get_nft_address_by_index": + return abi.GetMethodDesc{ + Name: "get_nft_address_by_index", + Arguments: []abi.VmValueDesc{{ + Name: "index", + StackType: "int", + Format: "bytes", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "address", + StackType: "slice", + Format: "addr", + }}, + }, nil + + case contract == known.JettonMinter && gm == "get_wallet_address": + return abi.GetMethodDesc{ + Name: "get_wallet_address", + Arguments: []abi.VmValueDesc{{ + Name: "owner_address", + StackType: "slice", + Format: "addr", + }}, + ReturnValues: []abi.VmValueDesc{{ + Name: "wallet_address", + StackType: "slice", + Format: "addr", + }}, + }, nil + } + + panic(fmt.Errorf("unknown %s get-method description for %s contract", contract, gm)) } func (m *mockContractRepo) AddOperation(_ context.Context, _ *core.ContractOperation) error { From 40dab8eda424e5e81f7d3f998254a3233fc4b82f Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 20 Mar 2024 20:09:39 +0700 Subject: [PATCH 163/186] docker-compose.dev.yml: bind ports to localhost --- docker-compose.dev.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5ca79a03..4bab316e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -15,8 +15,8 @@ services: <<: *anton-rewrites clickhouse: ports: - - "9000:9000" - - "8123:8123" + - "127.0.0.1:9000:9000" + - "127.0.0.1:8123:8123" postgres: ports: - - "5432:5432" + - "127.0.0.1:5432:5432" From 40e6a9c2ba07fe6cd52647180573c0d24d9c4f4c Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 28 Mar 2024 14:15:40 +0700 Subject: [PATCH 164/186] [repo] AddTransactions and AddAccountStates: bulk insert --- internal/core/repository/account/account.go | 10 +++++----- internal/core/repository/tx/tx.go | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 7aa48efb..c94d9e73 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -204,11 +204,11 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ for _, executions := range a.ExecutedGetMethods { sort.Slice(executions, func(i, j int) bool { return executions[i].Name < executions[j].Name }) } + } - _, err := tx.NewInsert().Model(a).Exec(ctx) - if err != nil { - return errors.Wrapf(err, "cannot insert new %s acc state", a.Address.String()) - } + _, err := tx.NewInsert().Model(&accounts).Exec(ctx) + if err != nil { + return errors.Wrapf(err, "cannot insert new account states") } addrTxLT := make(map[addr.Address]uint64) @@ -233,7 +233,7 @@ func (r *Repository) AddAccountStates(ctx context.Context, tx bun.Tx, accounts [ } } - _, 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/tx/tx.go b/internal/core/repository/tx/tx.go index 4d262969..43eea430 100644 --- a/internal/core/repository/tx/tx.go +++ b/internal/core/repository/tx/tx.go @@ -104,15 +104,16 @@ func (r *Repository) AddTransactions(ctx context.Context, tx bun.Tx, transaction if len(transactions) == 0 { return nil } - for _, t := range transactions { - _, err := tx.NewInsert().Model(t).Exec(ctx) - if err != nil { - return err - } + + _, err := tx.NewInsert().Model(&transactions).Exec(ctx) + if err != nil { + return err } - _, err := r.ch.NewInsert().Model(&transactions).Exec(ctx) + + _, err = r.ch.NewInsert().Model(&transactions).Exec(ctx) if err != nil { return err } + return nil } From 692250b4125da20529653b367c11953d17a91913 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 1 Apr 2024 22:00:33 +0700 Subject: [PATCH 165/186] [app] getLastSeenAccountState: get previous minter state by item tx lt --- internal/app/fetcher/account.go | 32 +++++++++++--------------------- internal/app/rescan/account.go | 26 +++++++++----------------- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 1d0b0714..264ed5bc 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -16,26 +16,16 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (s *Service) getLastSeenAccountState(ctx context.Context, master, b *ton.BlockIDExt, a addr.Address) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) - - var latestBlock *core.BlockID - switch { - case b.Workchain == int32(a.Workchain()): - latestBlock = &core.BlockID{Workchain: b.Workchain, Shard: b.Shard, SeqNo: b.SeqNo} - case master.Workchain == int32(a.Workchain()): - latestBlock = &core.BlockID{Workchain: master.Workchain, Shard: master.Shard, SeqNo: master.SeqNo} - default: - return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) - } +func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { + defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%s, %d)", a.String(), lastLT) + + lastLT++ accountReq := filter.AccountsReq{ - Addresses: []*addr.Address{&a}, - Workchain: &latestBlock.Workchain, - Shard: &latestBlock.Shard, - BlockSeqNoLeq: &latestBlock.SeqNo, - Order: "DESC", - Limit: 1, + Addresses: []*addr.Address{&a}, + Order: "DESC", + AfterTxLT: &lastLT, + Limit: 1, } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) if err != nil { @@ -48,7 +38,7 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, master, b *ton.Bl return accountRes.Rows[0], nil } -func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { +func (s *Service) makeGetOtherAccountFunc(b *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { // first attempt is to look for an account in this given block acc, ok := s.accounts.get(b, a) @@ -57,7 +47,7 @@ func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt) func(ctx co } // second attempt is to look for the latest account state in the database - acc, err := s.getLastSeenAccountState(ctx, master, b, a) + acc, err := s.getLastSeenAccountState(ctx, a, lastLT) if err == nil { return acc, nil } @@ -126,7 +116,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a // sometimes, to parse the full account data we need to get other contracts states // for example, to get nft item data - getOtherAccount := s.makeGetOtherAccountFunc(master, b) + getOtherAccount := s.makeGetOtherAccountFunc(b, acc.LastTxLT) err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 70a09065..08fe4027 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -16,24 +16,16 @@ import ( "github.com/tonindexer/anton/internal/core/filter" ) -func (s *Service) getRecentAccountState(ctx context.Context, b core.BlockID, a addr.Address) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%d, %d, %d, %s)", b.Workchain, b.Shard, b.SeqNo, a.String()) +func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { + defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%s, %d)", a.String(), lastLT) - var boundBlock core.BlockID - switch { - case b.Workchain == int32(a.Workchain()): - boundBlock = b - default: - return nil, errors.Wrapf(core.ErrInvalidArg, "address is in %d workchain, but the given block is from %d workchain", a.Workchain(), b.Workchain) - } + lastLT++ accountReq := filter.AccountsReq{ - Addresses: []*addr.Address{&a}, - Workchain: &boundBlock.Workchain, - Shard: &boundBlock.Shard, - BlockSeqNoLeq: &boundBlock.SeqNo, - Order: "DESC", - Limit: 1, + Addresses: []*addr.Address{&a}, + Order: "DESC", + AfterTxLT: &lastLT, + Limit: 1, } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) if err != nil { @@ -104,7 +96,7 @@ func (s *Service) parseAccountData(ctx context.Context, task *core.RescanTask, a } getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - return s.getRecentAccountState(ctx, acc.BlockID(), a) + return s.getRecentAccountState(ctx, a, acc.LastTxLT) } err := s.Parser.ParseAccountContractData(ctx, task.Contract, acc, getOtherAccountFunc) @@ -173,7 +165,7 @@ func (s *Service) clearExecutedGetMethod(task *core.RescanTask, acc *core.Accoun func (s *Service) executeGetMethod(ctx context.Context, task *core.RescanTask, acc *core.AccountState, gm string) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { - return s.getRecentAccountState(ctx, acc.BlockID(), a) + return s.getRecentAccountState(ctx, a, acc.LastTxLT) } err := s.Parser.ExecuteAccountGetMethod(ctx, task.ContractName, gm, acc, getOtherAccountFunc) From 9c7b13d43b3f842160351ea1ff4efaee8a52b691 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Mon, 1 Apr 2024 22:09:41 +0700 Subject: [PATCH 166/186] [fetcher] getOtherAccountFunc: GetAccount by master block --- internal/app/fetcher/account.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/app/fetcher/account.go b/internal/app/fetcher/account.go index 264ed5bc..91a0eba4 100644 --- a/internal/app/fetcher/account.go +++ b/internal/app/fetcher/account.go @@ -38,7 +38,7 @@ func (s *Service) getLastSeenAccountState(ctx context.Context, a addr.Address, l return accountRes.Rows[0], nil } -func (s *Service) makeGetOtherAccountFunc(b *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { +func (s *Service) makeGetOtherAccountFunc(master, b *ton.BlockIDExt, lastLT uint64) func(ctx context.Context, a addr.Address) (*core.AccountState, error) { getOtherAccountFunc := func(ctx context.Context, a addr.Address) (*core.AccountState, error) { // first attempt is to look for an account in this given block acc, ok := s.accounts.get(b, a) @@ -58,7 +58,7 @@ func (s *Service) makeGetOtherAccountFunc(b *ton.BlockIDExt, lastLT uint64) func lvl.Err(err).Str("addr", a.Base64()).Msg("get latest other account state") // third attempt is to get needed contract state from the node - raw, err := s.API.GetAccount(ctx, b, a.MustToTonutils()) + raw, err := s.API.GetAccount(ctx, master, a.MustToTonutils()) if err != nil { return nil, errors.Wrapf(err, "cannot get %s account state", a.Base64()) } @@ -116,7 +116,7 @@ func (s *Service) getAccount(ctx context.Context, master, b *ton.BlockIDExt, a a // sometimes, to parse the full account data we need to get other contracts states // for example, to get nft item data - getOtherAccount := s.makeGetOtherAccountFunc(b, acc.LastTxLT) + getOtherAccount := s.makeGetOtherAccountFunc(master, b, acc.LastTxLT) err = s.Parser.ParseAccountData(ctx, acc, getOtherAccount) if err != nil && !errors.Is(err, app.ErrImpossibleParsing) { From fa55e5faa237cb87aebef9380b42b7bc0061f9d6 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 2 Apr 2024 19:39:42 +0700 Subject: [PATCH 167/186] [rescan] getRecentAccountState: add cache for minter account states --- internal/app/rescan/account.go | 28 ++++++++++-- internal/app/rescan/cache.go | 80 +++++++++++++++++++++++++++++++++- internal/app/rescan/rescan.go | 6 ++- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 08fe4027..37550a64 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -17,14 +17,17 @@ import ( ) func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { - defer app.TimeTrack(time.Now(), "getLastSeenAccountState(%s, %d)", a.String(), lastLT) + defer app.TimeTrack(time.Now(), "getRecentAccountState(%s, %d)", a.String(), lastLT) - lastLT++ + if minter, ok := s.minterStateCache.get(a, lastLT); ok { + return minter, nil + } + beforeTxLT := lastLT + 1 accountReq := filter.AccountsReq{ Addresses: []*addr.Address{&a}, Order: "DESC", - AfterTxLT: &lastLT, + AfterTxLT: &beforeTxLT, Limit: 1, } accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) @@ -35,6 +38,25 @@ func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, las return nil, errors.Wrap(core.ErrNotFound, "could not find needed account state") } + afterTxLT := lastLT - 1 + accountReq = filter.AccountsReq{ + Addresses: []*addr.Address{&a}, + Order: "ASC", + AfterTxLT: &afterTxLT, + Limit: 1, + } + nextAccountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) + if err != nil { + return nil, errors.Wrap(err, "filter accounts for next minter state") + } + + var nextMinterTxLT uint64 + if len(nextAccountRes.Rows) > 0 { + nextMinterTxLT = nextAccountRes.Rows[0].LastTxLT + } + + s.minterStateCache.put(a, accountRes.Rows[0], nextMinterTxLT) + return accountRes.Rows[0], nil } diff --git a/internal/app/rescan/cache.go b/internal/app/rescan/cache.go index cff31d65..b46e1b23 100644 --- a/internal/app/rescan/cache.go +++ b/internal/app/rescan/cache.go @@ -6,6 +6,7 @@ import ( "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" + "github.com/tonindexer/anton/internal/core" ) type interfacesCacheItem struct { @@ -13,7 +14,7 @@ type interfacesCacheItem struct { keyPtr *list.Element } -// accountInterfacesCache implements LRU cache for account interfaces. +// interfacesCache implements LRU cache for account interfaces. // For a given address it stores account interface updates. // Used only in messages rescan. type interfacesCache struct { @@ -71,3 +72,80 @@ func (c *interfacesCache) get(key addr.Address) (map[uint64][]abi.ContractName, return nil, false } + +type minterStateCacheItem struct { + state *core.AccountState + nextTxLT uint64 + keyPtr *list.Element +} + +// minterStateCache implements LRU cache for minter account states, +// which are used for rescanning of nft items and jetton wallets. +type minterStateCache struct { + list *list.List + items map[addr.Address]*minterStateCacheItem + capacity int + sync.RWMutex +} + +func newMinterStateCache(capacity int) *minterStateCache { + return &minterStateCache{ + list: list.New(), + items: map[addr.Address]*minterStateCacheItem{}, + capacity: capacity, + } +} + +func (c *minterStateCache) removeItem() { + back := c.list.Back() + c.list.Remove(back) + delete(c.items, back.Value.(addr.Address)) //nolint:forcetypeassert // no need +} + +func (c *minterStateCache) updateItem(item *minterStateCacheItem, k addr.Address, state *core.AccountState, nextTxLT uint64) { + item.state = state + item.nextTxLT = nextTxLT + c.items[k] = item + c.list.MoveToFront(item.keyPtr) +} + +func (c *minterStateCache) put(k addr.Address, state *core.AccountState, nextTxLT uint64) { + c.Lock() + defer c.Unlock() + + if item, ok := c.items[k]; !ok { + if c.capacity == len(c.items) { + c.removeItem() + } + c.items[k] = &minterStateCacheItem{ + state: state, + nextTxLT: nextTxLT, + keyPtr: c.list.PushFront(k), + } + } else { + c.updateItem(item, k, state, nextTxLT) // it's never used + } +} + +func (c *minterStateCache) get(key addr.Address, itemTxLT uint64) (state *core.AccountState, ok bool) { + c.Lock() + defer c.Unlock() + + item, ok := c.items[key] + if !ok { + return nil, false + } + + c.list.MoveToFront(item.keyPtr) + + if item.nextTxLT != 0 && itemTxLT > item.nextTxLT { + // as we are processing item state, which is later than the next minter state we saw, + // we should remove the old minter state and get the new one from the database + front := c.list.Front() + c.list.Remove(front) + delete(c.items, front.Value.(addr.Address)) //nolint:forcetypeassert // no need + return nil, false + } + + return item.state, true +} diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index cf8e61cd..b3dd82f5 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -21,7 +21,8 @@ var _ app.RescanService = (*Service)(nil) type Service struct { *app.RescanConfig - interfacesCache *interfacesCache + interfacesCache *interfacesCache + minterStateCache *minterStateCache run bool mx sync.RWMutex @@ -38,7 +39,8 @@ func NewService(cfg *app.RescanConfig) *Service { s.Workers = 1 } - s.interfacesCache = newInterfacesCache(16384) // number of addresses + s.interfacesCache = newInterfacesCache(16384) // number of addresses + s.minterStateCache = newMinterStateCache(16384) // number of addresses return s } From 49bd9403e299f9876c0cc2cc073f58b03c649f52 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 2 Apr 2024 19:54:57 +0700 Subject: [PATCH 168/186] [parser] checkMinter: fix emulateGetMethod error log --- internal/app/parser/get.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/parser/get.go b/internal/app/parser/get.go index 9d27a996..46f2ce91 100644 --- a/internal/app/parser/get.go +++ b/internal/app/parser/get.go @@ -186,7 +186,7 @@ func (s *Service) checkMinter(ctx context.Context, minter, item *core.AccountSta appendGetMethodExecution(item, i, &exec) if exec.Error != "" { - log.Error().Err(err).Msgf("execute %s %s get-method", desc.Name, i) + log.Error().Str("exec_error", exec.Error).Msgf("execute %s %s get-method", desc.Name, i) return } From 0611e49a37eddfee834be81403ee3953af0b0037 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Tue, 2 Apr 2024 20:43:24 +0700 Subject: [PATCH 169/186] [rescan] minter states cache: check current minter state tx lt --- internal/app/rescan/cache.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/app/rescan/cache.go b/internal/app/rescan/cache.go index b46e1b23..068bbcea 100644 --- a/internal/app/rescan/cache.go +++ b/internal/app/rescan/cache.go @@ -131,21 +131,24 @@ func (c *minterStateCache) get(key addr.Address, itemTxLT uint64) (state *core.A c.Lock() defer c.Unlock() - item, ok := c.items[key] + minter, ok := c.items[key] if !ok { return nil, false } - c.list.MoveToFront(item.keyPtr) + c.list.MoveToFront(minter.keyPtr) + + if itemTxLT < minter.state.LastTxLT || (minter.nextTxLT != 0 && itemTxLT > minter.nextTxLT) { + // as we are processing item state, + // which is later than the next minter state or earlier than the current minter state in cache, + // we should remove the current minter state and get the new one from the database - if item.nextTxLT != 0 && itemTxLT > item.nextTxLT { - // as we are processing item state, which is later than the next minter state we saw, - // we should remove the old minter state and get the new one from the database front := c.list.Front() c.list.Remove(front) delete(c.items, front.Value.(addr.Address)) //nolint:forcetypeassert // no need + return nil, false } - return item.state, true + return minter.state, true } From ce7addb32ada23d9888efc63e268465a761dfe29 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 3 Apr 2024 19:08:51 +0700 Subject: [PATCH 170/186] add LRU cache module --- lru/cache.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lru/cache.go diff --git a/lru/cache.go b/lru/cache.go new file mode 100644 index 00000000..f46612f0 --- /dev/null +++ b/lru/cache.go @@ -0,0 +1,77 @@ +package lru + +import ( + "container/list" + "sync" +) + +type Item[V any] struct { + data V + keyPtr *list.Element +} + +type Cache[K comparable, V any] struct { + queue *list.List + items map[K]*Item[V] + capacity int + sync.Mutex +} + +func New[K comparable, V any](capacity int) *Cache[K, V] { + return &Cache[K, V]{ + queue: list.New(), + items: map[K]*Item[V]{}, + capacity: capacity, + } +} + +func (c *Cache[K, V]) removeItem() { + back := c.queue.Back() + c.queue.Remove(back) + delete(c.items, back.Value.(K)) //nolint:forcetypeassert // no need +} + +func (c *Cache[K, V]) updateItem(item *Item[V], k K, v V) { + item.data = v + c.items[k] = item + c.queue.MoveToFront(item.keyPtr) +} + +func (c *Cache[K, V]) Put(k K, v V) { + c.Lock() + defer c.Unlock() + + if item, ok := c.items[k]; !ok { + if c.capacity == len(c.items) { + c.removeItem() + } + c.items[k] = &Item[V]{ + data: v, + keyPtr: c.queue.PushFront(k), + } + } else { + c.updateItem(item, k, v) // actually it's not used + } +} + +func (c *Cache[K, V]) Get(key K) (v V, ok bool) { + c.Lock() + defer c.Unlock() + + if item, ok := c.items[key]; ok { + c.queue.MoveToFront(item.keyPtr) + return item.data, true + } + + return v, false +} + +func (c *Cache[K, V]) Keys() (keys []K) { + c.Lock() + defer c.Unlock() + + for k := range c.items { + keys = append(keys, k) + } + return keys +} From 7b9bb8684b85d676b05357a6ecf007f1184c3075 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 3 Apr 2024 19:15:23 +0700 Subject: [PATCH 171/186] [lru] do not expose mutex, add ireturn nolint directive for Get method --- lru/cache.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lru/cache.go b/lru/cache.go index f46612f0..ee011d95 100644 --- a/lru/cache.go +++ b/lru/cache.go @@ -14,7 +14,7 @@ type Cache[K comparable, V any] struct { queue *list.List items map[K]*Item[V] capacity int - sync.Mutex + mx sync.Mutex } func New[K comparable, V any](capacity int) *Cache[K, V] { @@ -38,8 +38,8 @@ func (c *Cache[K, V]) updateItem(item *Item[V], k K, v V) { } func (c *Cache[K, V]) Put(k K, v V) { - c.Lock() - defer c.Unlock() + c.mx.Lock() + defer c.mx.Unlock() if item, ok := c.items[k]; !ok { if c.capacity == len(c.items) { @@ -54,9 +54,9 @@ func (c *Cache[K, V]) Put(k K, v V) { } } -func (c *Cache[K, V]) Get(key K) (v V, ok bool) { - c.Lock() - defer c.Unlock() +func (c *Cache[K, V]) Get(key K) (v V, ok bool) { //nolint:ireturn // returns generic interface (V) of type param any + c.mx.Lock() + defer c.mx.Unlock() if item, ok := c.items[key]; ok { c.queue.MoveToFront(item.keyPtr) @@ -67,8 +67,8 @@ func (c *Cache[K, V]) Get(key K) (v V, ok bool) { } func (c *Cache[K, V]) Keys() (keys []K) { - c.Lock() - defer c.Unlock() + c.mx.Lock() + defer c.mx.Unlock() for k := range c.items { keys = append(keys, k) From 2b31179656b841cc1501baa7fddb3105d61960f3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 3 Apr 2024 19:17:31 +0700 Subject: [PATCH 172/186] [rescan] refactor minter states cache to handle multiple states of an address --- internal/app/rescan/cache.go | 166 ++++++++++------------------------ internal/app/rescan/rescan.go | 11 ++- internal/app/rescan/tx.go | 4 +- 3 files changed, 55 insertions(+), 126 deletions(-) diff --git a/internal/app/rescan/cache.go b/internal/app/rescan/cache.go index 068bbcea..f202e17f 100644 --- a/internal/app/rescan/cache.go +++ b/internal/app/rescan/cache.go @@ -1,154 +1,80 @@ package rescan import ( - "container/list" - "sync" + "sort" + + "github.com/rs/zerolog/log" - "github.com/tonindexer/anton/abi" "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" + "github.com/tonindexer/anton/lru" ) -type interfacesCacheItem struct { - data map[uint64][]abi.ContractName - keyPtr *list.Element -} - -// interfacesCache implements LRU cache for account interfaces. -// For a given address it stores account interface updates. -// Used only in messages rescan. -type interfacesCache struct { - queue *list.List - items map[addr.Address]*interfacesCacheItem - capacity int - sync.RWMutex -} - -func newInterfacesCache(capacity int) *interfacesCache { - return &interfacesCache{ - queue: list.New(), - items: map[addr.Address]*interfacesCacheItem{}, - capacity: capacity, - } -} - -func (c *interfacesCache) removeItem() { - back := c.queue.Back() - c.queue.Remove(back) - delete(c.items, back.Value.(addr.Address)) //nolint:forcetypeassert // no need -} - -func (c *interfacesCache) updateItem(item *interfacesCacheItem, k addr.Address, v map[uint64][]abi.ContractName) { - item.data = v - c.items[k] = item - c.queue.MoveToFront(item.keyPtr) -} - -func (c *interfacesCache) put(k addr.Address, v map[uint64][]abi.ContractName) { - c.Lock() - defer c.Unlock() - - if item, ok := c.items[k]; !ok { - if c.capacity == len(c.items) { - c.removeItem() - } - c.items[k] = &interfacesCacheItem{ - data: v, - keyPtr: c.queue.PushFront(k), - } - } else { - c.updateItem(item, k, v) // actually it's not used - } -} - -func (c *interfacesCache) get(key addr.Address) (map[uint64][]abi.ContractName, bool) { - c.RLock() - defer c.RUnlock() +const maxMinterStates int = 64 - if item, ok := c.items[key]; ok { - c.queue.MoveToFront(item.keyPtr) - return item.data, true - } - - return nil, false -} - -type minterStateCacheItem struct { - state *core.AccountState +type minterState struct { + acc *core.AccountState nextTxLT uint64 - keyPtr *list.Element } -// minterStateCache implements LRU cache for minter account states, -// which are used for rescanning of nft items and jetton wallets. -type minterStateCache struct { - list *list.List - items map[addr.Address]*minterStateCacheItem - capacity int - sync.RWMutex +type minterStatesCache struct { + lru *lru.Cache[uint64, *minterState] } -func newMinterStateCache(capacity int) *minterStateCache { - return &minterStateCache{ - list: list.New(), - items: map[addr.Address]*minterStateCacheItem{}, - capacity: capacity, +func newMinterStatesCache() *minterStatesCache { + return &minterStatesCache{ + lru: lru.New[uint64, *minterState](maxMinterStates), } } -func (c *minterStateCache) removeItem() { - back := c.list.Back() - c.list.Remove(back) - delete(c.items, back.Value.(addr.Address)) //nolint:forcetypeassert // no need +type mintersCache struct { + lru *lru.Cache[addr.Address, *minterStatesCache] } -func (c *minterStateCache) updateItem(item *minterStateCacheItem, k addr.Address, state *core.AccountState, nextTxLT uint64) { - item.state = state - item.nextTxLT = nextTxLT - c.items[k] = item - c.list.MoveToFront(item.keyPtr) +func newMinterStateCache(capacity int) *mintersCache { + return &mintersCache{ + lru: lru.New[addr.Address, *minterStatesCache](capacity), + } } -func (c *minterStateCache) put(k addr.Address, state *core.AccountState, nextTxLT uint64) { - c.Lock() - defer c.Unlock() - - if item, ok := c.items[k]; !ok { - if c.capacity == len(c.items) { - c.removeItem() - } - c.items[k] = &minterStateCacheItem{ - state: state, - nextTxLT: nextTxLT, - keyPtr: c.list.PushFront(k), - } - } else { - c.updateItem(item, k, state, nextTxLT) // it's never used +func (c *mintersCache) put(k addr.Address, state *core.AccountState, nextTxLT uint64) { + states, ok := c.lru.Get(k) + if !ok { + states = newMinterStatesCache() + states.lru.Put(state.LastTxLT, &minterState{acc: state, nextTxLT: nextTxLT}) + c.lru.Put(k, states) + return } -} -func (c *minterStateCache) get(key addr.Address, itemTxLT uint64) (state *core.AccountState, ok bool) { - c.Lock() - defer c.Unlock() + states.lru.Put(state.LastTxLT, &minterState{acc: state, nextTxLT: nextTxLT}) +} - minter, ok := c.items[key] +func (c *mintersCache) get(k addr.Address, itemTxLT uint64) (state *core.AccountState, ok bool) { + states, ok := c.lru.Get(k) if !ok { return nil, false } - c.list.MoveToFront(minter.keyPtr) + lts := states.lru.Keys() + sort.Slice(lts, func(i, j int) bool { return lts[i] > lts[j] }) - if itemTxLT < minter.state.LastTxLT || (minter.nextTxLT != 0 && itemTxLT > minter.nextTxLT) { - // as we are processing item state, - // which is later than the next minter state or earlier than the current minter state in cache, - // we should remove the current minter state and get the new one from the database + for _, lt := range lts { + if lt > itemTxLT { + continue + } - front := c.list.Front() - c.list.Remove(front) - delete(c.items, front.Value.(addr.Address)) //nolint:forcetypeassert // no need + minter, ok := states.lru.Get(lt) + if !ok { + log.Error().Str("addr", k.Base64()).Uint64("last_tx_lt", lt).Msg("cannot get minter state from cache") + return nil, false + } - return nil, false + if minter.nextTxLT != 0 && itemTxLT > minter.nextTxLT { + continue + } + + return minter.acc, false } - return minter.state, true + return nil, false } diff --git a/internal/app/rescan/rescan.go b/internal/app/rescan/rescan.go index b3dd82f5..789458ac 100644 --- a/internal/app/rescan/rescan.go +++ b/internal/app/rescan/rescan.go @@ -11,9 +11,12 @@ import ( "github.com/rs/zerolog/log" "github.com/xssnick/tonutils-go/tvm/cell" + "github.com/tonindexer/anton/abi" + "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/filter" + "github.com/tonindexer/anton/lru" ) var _ app.RescanService = (*Service)(nil) @@ -21,8 +24,8 @@ var _ app.RescanService = (*Service)(nil) type Service struct { *app.RescanConfig - interfacesCache *interfacesCache - minterStateCache *minterStateCache + interfacesCache *lru.Cache[addr.Address, map[uint64][]abi.ContractName] + minterStateCache *mintersCache run bool mx sync.RWMutex @@ -39,8 +42,8 @@ func NewService(cfg *app.RescanConfig) *Service { s.Workers = 1 } - s.interfacesCache = newInterfacesCache(16384) // number of addresses - s.minterStateCache = newMinterStateCache(16384) // number of addresses + s.interfacesCache = lru.New[addr.Address, map[uint64][]abi.ContractName](16384) // number of addresses + s.minterStateCache = newMinterStateCache(2048) // number of addresses return s } diff --git a/internal/app/rescan/tx.go b/internal/app/rescan/tx.go index 2b607222..696522e2 100644 --- a/internal/app/rescan/tx.go +++ b/internal/app/rescan/tx.go @@ -40,7 +40,7 @@ func (s *Service) chooseInterfaces(updates map[uint64][]abi.ContractName, txLT u } func (s *Service) getAccountStateForMessage(ctx context.Context, a addr.Address, txLT uint64) *core.AccountState { - interfaceUpdates, ok := s.interfacesCache.get(a) + interfaceUpdates, ok := s.interfacesCache.Get(a) if ok { return &core.AccountState{Address: a, Types: s.chooseInterfaces(interfaceUpdates, txLT)} } @@ -51,7 +51,7 @@ func (s *Service) getAccountStateForMessage(ctx context.Context, a addr.Address, return nil } - s.interfacesCache.put(a, interfaceUpdates) + s.interfacesCache.Put(a, interfaceUpdates) return &core.AccountState{Address: a, Types: s.chooseInterfaces(interfaceUpdates, txLT)} } From 071b0d9b9520a4e603fa84f7b073434815e06689 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 3 Apr 2024 19:20:40 +0700 Subject: [PATCH 173/186] Dockerfile: copy lru module --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 889a42d6..4f94c474 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ RUN go mod download # copy application code COPY migrations /go/src/github.com/tonindexer/anton/migrations +COPY lru /go/src/github.com/tonindexer/anton/lru COPY cmd /go/src/github.com/tonindexer/anton/cmd COPY addr /go/src/github.com/tonindexer/anton/addr COPY abi /go/src/github.com/tonindexer/anton/abi From a77574dc1fe57e8a241c12999bdb80346022b35e Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 3 Apr 2024 19:21:48 +0700 Subject: [PATCH 174/186] Dockerfile: add new line at the end of file --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4f94c474..73ba7220 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,4 +71,4 @@ COPY --from=builder /anton /usr/bin/anton USER anton:anton EXPOSE 8080 -ENTRYPOINT ["/usr/bin/anton"] \ No newline at end of file +ENTRYPOINT ["/usr/bin/anton"] From b610c4f17fc49379a316961a8832de8b90872c0d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 4 Apr 2024 22:12:51 +0700 Subject: [PATCH 175/186] [rescan] getRecentAccountState: select many minter states at once --- internal/app/rescan/account.go | 37 ++----- internal/core/account.go | 3 + internal/core/repository/account/account.go | 103 +++++++++++++++++++- 3 files changed, 113 insertions(+), 30 deletions(-) diff --git a/internal/app/rescan/account.go b/internal/app/rescan/account.go index 37550a64..bc637f19 100644 --- a/internal/app/rescan/account.go +++ b/internal/app/rescan/account.go @@ -13,7 +13,6 @@ import ( "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/app" "github.com/tonindexer/anton/internal/core" - "github.com/tonindexer/anton/internal/core/filter" ) func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, lastLT uint64) (*core.AccountState, error) { @@ -23,41 +22,21 @@ func (s *Service) getRecentAccountState(ctx context.Context, a addr.Address, las return minter, nil } - beforeTxLT := lastLT + 1 - accountReq := filter.AccountsReq{ - Addresses: []*addr.Address{&a}, - Order: "DESC", - AfterTxLT: &beforeTxLT, - Limit: 1, - } - accountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) + states, err := s.AccountRepo.GetAllAccountStates(ctx, a, lastLT, maxMinterStates) if err != nil { - return nil, errors.Wrap(err, "filter accounts") - } - if len(accountRes.Rows) < 1 { - return nil, errors.Wrap(core.ErrNotFound, "could not find needed account state") + return nil, err } - afterTxLT := lastLT - 1 - accountReq = filter.AccountsReq{ - Addresses: []*addr.Address{&a}, - Order: "ASC", - AfterTxLT: &afterTxLT, - Limit: 1, - } - nextAccountRes, err := s.AccountRepo.FilterAccounts(ctx, &accountReq) - if err != nil { - return nil, errors.Wrap(err, "filter accounts for next minter state") + for nextMinterTxLT, state := range states { + s.minterStateCache.put(a, state, nextMinterTxLT) } - var nextMinterTxLT uint64 - if len(nextAccountRes.Rows) > 0 { - nextMinterTxLT = nextAccountRes.Rows[0].LastTxLT + minter, ok := s.minterStateCache.get(a, lastLT) + if !ok { + return nil, errors.Wrapf(core.ErrNotFound, "cannot find %s minter state before %d lt", a.Base64(), lastLT) } - s.minterStateCache.put(a, accountRes.Rows[0], nextMinterTxLT) - - return accountRes.Rows[0], nil + return minter, nil } func copyAccountState(state *core.AccountState) *core.AccountState { diff --git a/internal/core/account.go b/internal/core/account.go index 321641f7..86f23c66 100644 --- a/internal/core/account.go +++ b/internal/core/account.go @@ -163,4 +163,7 @@ type AccountRepository interface { // GetAllAccountInterfaces returns transaction LT, on which contract interface was updated. // It also considers, that contract can be both upgraded and downgraded. GetAllAccountInterfaces(context.Context, addr.Address) (map[uint64][]abi.ContractName, error) + + // GetAllAccountStates is pretty much similar to GetAllAccountInterfaces, but it returns updates of code or data. + GetAllAccountStates(ctx context.Context, a addr.Address, beforeTxLT uint64, limit int) (map[uint64]*AccountState, error) } diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index c94d9e73..4b9a268e 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -1,6 +1,7 @@ package account import ( + "bytes" "context" "database/sql" "encoding/json" @@ -338,7 +339,7 @@ func (r *Repository) MatchStatesByInterfaceDesc(ctx context.Context, func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address) (map[uint64][]abi.ContractName, error) { var ret []struct { ChangeTxLT int64 - ChangeTypes []abi.ContractName `ch:"type:Array(String)" ` + ChangeTypes []abi.ContractName `ch:"type:Array(String)"` } minTxLtSubQ := r.ch.NewSelect().Model((*core.AccountState)(nil)). @@ -388,3 +389,103 @@ func (r *Repository) GetAllAccountInterfaces(ctx context.Context, a addr.Address return res, nil } + +func (r *Repository) GetAllAccountStates(ctx context.Context, a addr.Address, beforeTxLT uint64, limit int) (map[uint64]*core.AccountState, error) { + var ret []struct { + ChangeTxLT int64 + ChangeCodeHash []byte `ch:"type:String"` + ChangeDataHash []byte `ch:"type:String"` + } + + minTxLtSubQ := r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("min(last_tx_lt)"). + Where("address = ?", &a). + Where("length(code_hash) > 0"). + Where("length(data_hash) > 0") + + err := r.ch.NewSelect(). + TableExpr("(?) AS sq", r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS change_tx_lt"). + ColumnExpr("code_hash AS change_code_hash"). + ColumnExpr("data_hash AS change_data_hash"). + Where("address = ? AND last_tx_lt = (?)", &a, minTxLtSubQ). + UnionAll( + r.ch.NewSelect(). + TableExpr("(?) AS diff", + r.ch.NewSelect().Model((*core.AccountState)(nil)). + ColumnExpr("last_tx_lt AS tx_lt"). + ColumnExpr("code_hash"). + ColumnExpr("data_hash"). + ColumnExpr("leadInFrame(last_tx_lt) OVER (ORDER BY last_tx_lt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_tx_lt"). + ColumnExpr("leadInFrame(code_hash) OVER (ORDER BY last_tx_lt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_code_hash"). + ColumnExpr("leadInFrame(data_hash) OVER (ORDER BY last_tx_lt ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_data_hash"). + Where("address = ?", &a). + Where("length(code_hash) > 0"). + Where("length(data_hash) > 0"). + Order("tx_lt ASC")). + ColumnExpr("if(next_tx_lt = 0, tx_lt, next_tx_lt) AS change_tx_lt"). + ColumnExpr("if(next_tx_lt = 0, code_hash, next_code_hash) AS change_code_hash"). + ColumnExpr("if(next_tx_lt = 0, data_hash, next_data_hash) AS change_data_hash"). + Where(` + code_hash != next_code_hash OR + data_hash != next_data_hash OR + next_tx_lt = 0`). + Order("change_tx_lt ASC"))). + Order("change_tx_lt ASC"). + Scan(ctx, &ret) + if err != nil { + return nil, err + } + + var ( + lastCodeHash, lastDataHash []byte + lts []uint64 + ) + for it := range ret { + if lastCodeHash != nil && bytes.Equal(ret[it].ChangeCodeHash, lastCodeHash) && bytes.Equal(ret[it].ChangeDataHash, lastDataHash) { + continue + } + lastCodeHash, lastDataHash = ret[it].ChangeCodeHash, ret[it].ChangeDataHash + lts = append(lts, uint64(ret[it].ChangeTxLT)) + } + + if len(lts) > limit { + var found bool + for it := range lts { + if lts[it] < beforeTxLT { + continue + } + if it >= limit { + lts = lts[it-limit : it] + } else { + lts = lts[0:limit] + } + found = true + break + } + if !found { + lts = lts[len(lts)-limit:] + } + } + + var states []*core.AccountState + err = r.pg.NewSelect().Model(&states). + Where("address = ?", a). + Where("last_tx_lt IN (?)", bun.In(lts)). + Order("last_tx_lt ASC"). + Scan(ctx) + if err != nil { + return nil, errors.Wrap(err, "select states by lts") + } + + res := make(map[uint64]*core.AccountState) + for it, s := range states { + var nextTxLT uint64 + if it != len(states)-1 { + nextTxLT = states[it+1].LastTxLT + } + res[nextTxLT] = s + } + + return res, nil +} From efa8d913eb28bfba425bf8264680d6b30cdfd9fc Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 4 Apr 2024 22:17:25 +0700 Subject: [PATCH 176/186] [repo] GetAllAccountStates: fix typo --- internal/core/repository/account/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 4b9a268e..dafe6680 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -470,7 +470,7 @@ func (r *Repository) GetAllAccountStates(ctx context.Context, a addr.Address, be var states []*core.AccountState err = r.pg.NewSelect().Model(&states). - Where("address = ?", a). + Where("address = ?", &a). Where("last_tx_lt IN (?)", bun.In(lts)). Order("last_tx_lt ASC"). Scan(ctx) From fe74fab24d2aa816828989162e755d8806655c48 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 4 Apr 2024 22:38:32 +0700 Subject: [PATCH 177/186] [repo] GetAllAccountStates: handle no states for an address --- internal/core/repository/account/account.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index dafe6680..8f366f56 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -437,6 +437,10 @@ func (r *Repository) GetAllAccountStates(ctx context.Context, a addr.Address, be return nil, err } + if len(ret) == 0 { + return nil, errors.Wrapf(core.ErrNotFound, "no account states for %s address", a.Base64()) + } + var ( lastCodeHash, lastDataHash []byte lts []uint64 From a8145611d1023152454664ec4a86c69e9356ebb3 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Thu, 4 Apr 2024 22:43:30 +0700 Subject: [PATCH 178/186] [rescan] mintersCache.get: fix typo on success return --- internal/app/rescan/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/rescan/cache.go b/internal/app/rescan/cache.go index f202e17f..d1a3f8c6 100644 --- a/internal/app/rescan/cache.go +++ b/internal/app/rescan/cache.go @@ -73,7 +73,7 @@ func (c *mintersCache) get(k addr.Address, itemTxLT uint64) (state *core.Account continue } - return minter.acc, false + return minter.acc, true } return nil, false From 2e38576dbbb0b01c8bc2c7ad8b7935c23e527c3b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 18:44:23 +0800 Subject: [PATCH 179/186] [query] addGetMethodDescription: heat up the cache --- 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 20637016..e3d0c84b 100644 --- a/internal/app/query/query.go +++ b/internal/app/query/query.go @@ -146,6 +146,9 @@ func (s *Service) fetchSkippedAccounts(ctx context.Context, req *filter.Accounts } func (s *Service) addGetMethodDescription(ctx context.Context, rows []*core.AccountState) error { + if _, err := s.contractRepo.GetInterfaces(ctx); err != nil { // to fill in the cache with contract interfaces + return errors.Wrapf(err, "get contract interfaces") + } for _, r := range rows { for name, methods := range r.ExecutedGetMethods { for it := range methods { From f14968ce4227e870b471a4d601383d0e3711eb5b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 18:45:48 +0800 Subject: [PATCH 180/186] [repo] update accounts and messages: debug log instead of info --- internal/core/repository/account/account.go | 2 +- internal/core/repository/msg/msg.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/repository/account/account.go b/internal/core/repository/account/account.go index 8f366f56..605eb519 100644 --- a/internal/core/repository/account/account.go +++ b/internal/core/repository/account/account.go @@ -246,7 +246,7 @@ func logAccountStateDataUpdate(acc *core.AccountState) { types, _ := json.Marshal(acc.Types) //nolint:errchkjson // no need getMethods, _ := json.Marshal(acc.ExecutedGetMethods) //nolint:errchkjson // no need - log.Info(). + log.Debug(). Str("address", acc.Address.Base64()). Uint64("last_tx_lt", acc.LastTxLT). RawJSON("types", types). diff --git a/internal/core/repository/msg/msg.go b/internal/core/repository/msg/msg.go index 361aafae..6bfe62bb 100644 --- a/internal/core/repository/msg/msg.go +++ b/internal/core/repository/msg/msg.go @@ -209,7 +209,7 @@ func (r *Repository) UpdateMessages(ctx context.Context, messages []*core.Messag } for _, msg := range messages { - log.Info(). + log.Debug(). Hex("msg_hash", msg.Hash). Str("src_address", msg.SrcAddress.Base64()). Str("src_contract", string(msg.SrcContract)). From e547efe6aead9bf042353dae61356af730ad6e0d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 19:14:59 +0800 Subject: [PATCH 181/186] [cmd] label: fix tonscan labels --- cmd/label/label.go | 104 ++++++++++++++++++---------------------- cmd/label/label_test.go | 2 +- 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/cmd/label/label.go b/cmd/label/label.go index 3e8e5daa..58e65a7a 100644 --- a/cmd/label/label.go +++ b/cmd/label/label.go @@ -1,7 +1,7 @@ package label import ( - "encoding/json" + "fmt" "io" "net/http" @@ -10,6 +10,8 @@ import ( "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" + "gopkg.in/yaml.v2" + "github.com/tonindexer/anton/addr" "github.com/tonindexer/anton/internal/core" "github.com/tonindexer/anton/internal/core/repository" @@ -17,91 +19,79 @@ import ( ) type tonscanLabel struct { - TonIcon string `json:"tonIcon"` - Name string `json:"name"` - IsScam bool `json:"isScam"` + Address string `yaml:"address"` + Name string `yaml:"name"` } -func isCEX(name string) bool { +func tonscanIsCexLabel(name string) bool { switch name { - case "CryptoBot", "CryptoBot Cold Storage", + case "Crypto Bot", "Crypto Bot Cold Storage", "Wallet Bot", "Old Wallet Bot", "OKX", "Bitfinex", "MEXC", "ByBit", "ByBit Witdrawal", "bit.com", "Bitpapa", "FixedFloat", "Huobi Deposit", "Huobi Withdrawal", "KuCoin Deposit", "KuCoin Withdrawal", - "FTX", - "BitGo FTX Bankruptcy Custody", + "FTX", "BitGo FTX Bankruptcy Custody", "EXMO", "EXMO Cold Storage 1", "EXMO Cold Storage 2", "EXMO Deposit", - "CoinEx", "Gate.io": + "CoinEx", "Gate.io", + "AvanChange", + "xRocket", + "Coinone Deposit", "Coinone Withdrawal": return true default: return false } } -func unmarshalTonscanLabel(addrStr string, j json.RawMessage) (*core.AddressLabel, error) { - var l tonscanLabel - var ret core.AddressLabel - - a := new(addr.Address) +func fetchTonscanLabels() ([]*core.AddressLabel, error) { + var ret []*core.AddressLabel - err := a.UnmarshalText([]byte(addrStr)) - if err != nil { - return nil, errors.Wrapf(err, "unmarshal %s address", addrStr) - } + // https://raw.githubusercontent.com/menschee/tonscanplus/main/data.json - ret.Address = *a + files := []string{"community.yaml", "exchanges.yaml", "people.yaml", "scam.yaml", "system.yaml", "validators.yaml"} - if j[0] == '"' { - ret.Name = string(j[1 : len(j)-1]) - } else { - err := json.Unmarshal(j, &l) + for _, f := range files { + resp, err := http.Get("https://raw.githubusercontent.com/catchain/address-book/master/source/" + f) if err != nil { return nil, err } - ret.Name = l.Name - if l.IsScam { - ret.Categories = append(ret.Categories, core.Scam) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("cannot fetch %s file, server returned %d code", f, resp.StatusCode) } - } - if isCEX(ret.Name) { - ret.Categories = append(ret.Categories, core.CentralizedExchange) - } - return &ret, nil -} - -func FetchTonscanLabels() ([]*core.AddressLabel, error) { - var ret []*core.AddressLabel + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } - // https://raw.githubusercontent.com/menschee/tonscanplus/main/data.json + var labels []*tonscanLabel + if err := yaml.Unmarshal(body, &labels); err != nil { + return nil, errors.Wrapf(err, "cannot yaml unmarshal %s file", f) + } - res, err := http.Get("https://raw.githubusercontent.com/catchain/tonscan/master/src/addrbook.json") - if err != nil { - return nil, err - } + for _, l := range labels { + a := new(addr.Address) - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } + err := a.UnmarshalText([]byte(l.Address)) + if err != nil { + return nil, errors.Wrapf(err, "unmarshal %s address", l.Address) + } - var addrMap = make(map[string]json.RawMessage) - if err := json.Unmarshal(body, &addrMap); err != nil { - return nil, errors.Wrap(err, "tonscan data unmarshal") - } + var categories []core.LabelCategory + if tonscanIsCexLabel(l.Name) { + categories = append(categories, core.CentralizedExchange) + } + if f == "scam.yaml" { + categories = append(categories, core.Scam) + } - for a, j := range addrMap { - l, err := unmarshalTonscanLabel(a, j) - if err != nil { - return nil, errors.Wrapf(err, "unmarshal %s label: %s", a, string(j)) - } - if l.Name == "Burn Address" { - continue + ret = append(ret, &core.AddressLabel{ + Address: *a, + Name: l.Name, + Categories: categories, + }) } - ret = append(ret, l) } return ret, nil @@ -125,7 +115,7 @@ var Command = &cli.Command{ var labels []*core.AddressLabel if ctx.Bool("tonscan") { - tonscan, err := FetchTonscanLabels() + tonscan, err := fetchTonscanLabels() if err != nil { return err } diff --git a/cmd/label/label_test.go b/cmd/label/label_test.go index d4e34ff0..9abd6366 100644 --- a/cmd/label/label_test.go +++ b/cmd/label/label_test.go @@ -7,7 +7,7 @@ import ( ) func TestFetchTonscanLabels(t *testing.T) { - label, err := FetchTonscanLabels() + label, err := fetchTonscanLabels() require.Nil(t, err) for _, l := range label { From 5625a990bf2420d30270aa8bd3fa5ed562642bc1 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 19:21:10 +0800 Subject: [PATCH 182/186] Dockerfile: add liblz4-dev to emulator-build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 73ba7220..665e3319 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ tzdata build-essential cmake clang openssl \ libssl-dev zlib1g-dev gperf wget git curl \ libreadline-dev ccache libmicrohttpd-dev ninja-build pkg-config \ - libsecp256k1-dev libsodium-dev + libsecp256k1-dev libsodium-dev liblz4-dev ADD --keep-git-dir=true https://github.com/ton-blockchain/ton.git /ton RUN cd /ton && git submodule update --init --recursive From 46317d7ae9276e475426f1012c870fb346196a65 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 19:52:53 +0800 Subject: [PATCH 183/186] [cmd] label: skip burn and system addresses --- cmd/label/label.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/label/label.go b/cmd/label/label.go index 58e65a7a..ca122e94 100644 --- a/cmd/label/label.go +++ b/cmd/label/label.go @@ -71,6 +71,11 @@ func fetchTonscanLabels() ([]*core.AddressLabel, error) { } for _, l := range labels { + switch l.Name { + case "Burn", "System": + continue + } + a := new(addr.Address) err := a.UnmarshalText([]byte(l.Address)) From fd0c5047699158be76690d78903477eee852555b Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 19:53:12 +0800 Subject: [PATCH 184/186] Dockerfile: add ca-certificates to application --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 665e3319..659db95c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,7 +60,7 @@ FROM debian:12.2-slim ENV LISTEN=0.0.0.0:8080 RUN apt-get update && \ - apt-get install -y libsecp256k1-1 libsodium23 libssl3 + apt-get install -y libsecp256k1-1 libsodium23 libssl3 ca-certificates RUN groupadd anton && useradd -g anton anton From 05a7bab2c1d367df048e919a56424009708e114d Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 19:57:31 +0800 Subject: [PATCH 185/186] [cmd] label: fix typo in Burn address name --- cmd/label/label.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/label/label.go b/cmd/label/label.go index ca122e94..7ba24e95 100644 --- a/cmd/label/label.go +++ b/cmd/label/label.go @@ -72,7 +72,7 @@ func fetchTonscanLabels() ([]*core.AddressLabel, error) { for _, l := range labels { switch l.Name { - case "Burn", "System": + case "Burn Address", "System": continue } From 681761e87e304e83fe9cf97bc06295889f8c6943 Mon Sep 17 00:00:00 2001 From: iam047801 Date: Wed, 10 Apr 2024 20:12:55 +0800 Subject: [PATCH 186/186] [migrations] fix contract definitions down migration --- .../20231014090410_contract_definitions_table.down.sql | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql b/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql index 87d60f3e..eb41e428 100644 --- a/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql +++ b/migrations/pgmigrations/20231014090410_contract_definitions_table.down.sql @@ -1,9 +1,3 @@ SET statement_timeout = 0; ---bun:split - -SELECT 1 - ---bun:split - -SELECT 2 +DROP TABLE contract_definitions;