diff --git a/cmd/api/main.go b/cmd/api/main.go index 45b942b9..990017ba 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -12,6 +12,7 @@ import ( "github.com/tonkeeper/opentonapi/pkg/blockchain" "github.com/tonkeeper/opentonapi/pkg/blockchain/indexer" "github.com/tonkeeper/opentonapi/pkg/config" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/opentonapi/pkg/litestorage" "github.com/tonkeeper/opentonapi/pkg/pyth" "github.com/tonkeeper/opentonapi/pkg/spam" @@ -59,7 +60,7 @@ func main() { pythFeeds := pyth.GetUpdatedWithFallback(context.Background(), log) storage, err := litestorage.NewLiteStorage( log, - client, + core.LiteAPIClient(client), litestorage.WithPreloadBlocks([]tongo.BlockID{ tongo.MustParseBlockID("(0,8000000000000000,72945279)"), }), @@ -92,7 +93,7 @@ func main() { if err != nil { log.Fatal("failed to create api handler", zap.Error(err)) } - idx := indexer.New(log, client) + idx := indexer.New(log, core.LiteAPIClient(client)) go idx.Run(context.TODO(), []chan indexer.IDandBlock{ storageBlockCh, }) diff --git a/pkg/addressbook/addressbook_test.go b/pkg/addressbook/addressbook_test.go index 4693e578..b5deee3d 100644 --- a/pkg/addressbook/addressbook_test.go +++ b/pkg/addressbook/addressbook_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tonkeeper/opentonapi/pkg/config" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/opentonapi/pkg/litestorage" "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/ton" @@ -91,7 +92,7 @@ func TestSearchAttachedAccountsByPrefix(t *testing.T) { client, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.NoError(t, err, "Failed to create lite API client") - liteStorage, err := litestorage.NewLiteStorage(logger, client) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(client)) require.NoError(t, err, "Failed to create lite storage") book := NewAddressBook(logger, config.AddressPath, config.JettonPath, config.CollectionPath, liteStorage) diff --git a/pkg/api/account_handlers_test.go b/pkg/api/account_handlers_test.go index 1dd63f57..6a1942fc 100644 --- a/pkg/api/account_handlers_test.go +++ b/pkg/api/account_handlers_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/opentonapi/pkg/spam" "github.com/tonkeeper/opentonapi/pkg/chainstate" @@ -65,7 +66,7 @@ func TestHandler_GetRawAccount(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -150,7 +151,7 @@ func TestHandler_GetBlockchainRawAccounts(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) h := &Handler{ storage: liteStorage, @@ -201,7 +202,7 @@ func TestHandler_GetAccount(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -274,7 +275,7 @@ func TestHandler_GetAccounts(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -360,7 +361,7 @@ func TestHandler_GetTransactions(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { diff --git a/pkg/api/blockchain_handlers_test.go b/pkg/api/blockchain_handlers_test.go index 010436fc..8c542195 100644 --- a/pkg/api/blockchain_handlers_test.go +++ b/pkg/api/blockchain_handlers_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/tonkeeper/opentonapi/pkg/addressbook" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/stretchr/testify/require" @@ -25,7 +26,7 @@ func TestHandler_GetRawBlockchainConfig(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -99,7 +100,7 @@ func TestHandler_GetRawBlockchainConfigFromBlock(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -157,7 +158,7 @@ func TestHandler_GetBlockchainConfigFromBlock(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -187,7 +188,7 @@ func TestHandler_GetBlockchainValidators(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { @@ -242,7 +243,7 @@ func TestHandler_GetBlockchainBlock(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { diff --git a/pkg/api/decode_message_test.go b/pkg/api/decode_message_test.go index e241c3c1..f3c533aa 100644 --- a/pkg/api/decode_message_test.go +++ b/pkg/api/decode_message_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/tonkeeper/opentonapi/pkg/addressbook" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/stretchr/testify/require" @@ -43,7 +44,7 @@ func TestHandler_DecodeMessage(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { diff --git a/pkg/api/event_handlers_test.go b/pkg/api/event_handlers_test.go index 039bb626..e4f60ea8 100644 --- a/pkg/api/event_handlers_test.go +++ b/pkg/api/event_handlers_test.go @@ -10,6 +10,7 @@ import ( "github.com/tonkeeper/opentonapi/pkg/addressbook" "github.com/tonkeeper/opentonapi/pkg/bath" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/opentonapi/pkg/spam" "github.com/tonkeeper/tongo" @@ -87,7 +88,7 @@ func TestHandler_EmulateMessageToAccountEvent(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 39bff942..d5152bc9 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -107,6 +107,7 @@ type Options struct { score scoreSource parallelTraceProcessing bool archiveLiteServers []config.LiteServer + archiveClient rewards.LiteClient publicAPIURL string } @@ -210,6 +211,18 @@ func WithArchiveLiteServers(s []config.LiteServer) Option { } } +// WithArchiveClient supplies the blockchain connection the rewards service +// reads validator history through, instead of NewHandler building one from the +// servers given to WithArchiveLiteServers. It is how a deployment running its +// own lightserver pool keeps the rewards service on that pool rather than +// opening a second one; when set, WithArchiveLiteServers is not used to +// construct a client. +func WithArchiveClient(cli rewards.LiteClient) Option { + return func(o *Options) { + o.archiveClient = cli + } +} + func WithPublicAPIURL(publicAPIURL string) Option { return func(o *Options) { o.publicAPIURL = publicAPIURL @@ -275,13 +288,24 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { slog.Warn("unable to detect tongo version", "err", err) } var rwd *rewards.Service - if len(options.archiveLiteServers) != 0 { - cli, err := rewards.NewClient(options.archiveLiteServers) - if err == nil { - rwd = rewards.New(cli, options.archiveLiteServers) - log.Println("rewards service initialized") - } else { - log.Println("rewards service unavailable:", err) + var stats *rewards.Stats + switch { + case options.archiveClient != nil: + // A supplied client keeps its own connections, so there is no server + // list to hand the service for rebuilding one. + rwd = rewards.New(options.archiveClient, nil) + stats = rewards.NewStatsWithClient(options.archiveClient) + log.Println("rewards service initialized on the supplied client") + default: + stats = rewards.NewStats(liteapi.WithLiteServers(options.archiveLiteServers)) + if len(options.archiveLiteServers) != 0 { + cli, err := rewards.NewClient(options.archiveLiteServers) + if err == nil { + rwd = rewards.New(cli, options.archiveLiteServers) + log.Println("rewards service initialized") + } else { + log.Println("rewards service unavailable:", err) + } } } return &Handler{ @@ -318,7 +342,7 @@ func NewHandler(logger *zap.Logger, opts ...Option) (*Handler, error) { tonConnect: tonConnect, configPool: configPool, rewards: rwd, - stats: rewards.NewStats(liteapi.WithLiteServers(options.archiveLiteServers)), + stats: stats, }, nil } diff --git a/pkg/api/jetton_handlers_test.go b/pkg/api/jetton_handlers_test.go index b0b0dc8f..3d54af63 100644 --- a/pkg/api/jetton_handlers_test.go +++ b/pkg/api/jetton_handlers_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/tonkeeper/opentonapi/pkg/addressbook" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo" @@ -38,7 +39,7 @@ func TestHandler_GetJettonsBalances(t *testing.T) { logger, _ := zap.NewDevelopment() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli, litestorage.WithKnownJettons([]tongo.AccountID{ + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli), litestorage.WithKnownJettons([]tongo.AccountID{ tongo.MustParseAddress("0:beb5d4638e860ccf7317296e298fde5b35982f4725b0676dc98b1de987b82ebc").ID, // Jetton kingy tongo.MustParseAddress("0:65de083a0007638233b6668354e50e44cd4225f1730d66b8b1f19e5d26690751").ID, // Lavandos tongo.MustParseAddress("0:274b605badfcecca83130b27cd375e6a73233f6e15d782a31dd2a80aff097cc0").ID, // fake jUSDT (with cyrillic T) diff --git a/pkg/api/staking_handlers_test.go b/pkg/api/staking_handlers_test.go index c6870481..afcd8154 100644 --- a/pkg/api/staking_handlers_test.go +++ b/pkg/api/staking_handlers_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/tonkeeper/opentonapi/pkg/addressbook" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/stretchr/testify/require" @@ -41,7 +42,7 @@ func TestHandler_GetStakingPoolInfo(t *testing.T) { logger := zap.L() cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - liteStorage, err := litestorage.NewLiteStorage(logger, cli) + liteStorage, err := litestorage.NewLiteStorage(logger, core.LiteAPIClient(cli)) require.Nil(t, err) book := &mockAddressBook{ OnGetAddressInfoByAddress: func(a tongo.AccountID) (addressbook.KnownAddress, bool) { diff --git a/pkg/bath/bath_test.go b/pkg/bath/bath_test.go index 0be75ae8..5adc8233 100644 --- a/pkg/bath/bath_test.go +++ b/pkg/bath/bath_test.go @@ -102,7 +102,7 @@ func isFocusedRun() bool { func newBathTestStorage(t *testing.T, cli *liteapi.Client, blocks []tongo.BlockID) *litestorage.LiteStorage { t.Helper() storage, err := litestorage.NewLiteStorage(zap.L(), - cli, + core.LiteAPIClient(cli), litestorage.WithPreloadBlocks(blocks), litestorage.WithPythPriceFeeds(pyth.Default), ) diff --git a/pkg/blockchain/indexer/indexer.go b/pkg/blockchain/indexer/indexer.go index 526ebb5d..bace1bd9 100644 --- a/pkg/blockchain/indexer/indexer.go +++ b/pkg/blockchain/indexer/indexer.go @@ -7,8 +7,8 @@ import ( "time" "github.com/sourcegraph/conc/iter" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" - "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" "go.uber.org/zap" ) @@ -22,10 +22,10 @@ type chunk struct { // Indexer tracks the blockchain and notifies subscribers about new blocks. type Indexer struct { logger *zap.Logger - cli *liteapi.Client + cli core.LiteClient } -func New(logger *zap.Logger, cli *liteapi.Client) *Indexer { +func New(logger *zap.Logger, cli core.LiteClient) *Indexer { return &Indexer{ cli: cli, logger: logger, diff --git a/pkg/blockchain/msg_sender.go b/pkg/blockchain/msg_sender.go index 79d5ef59..05b2c454 100644 --- a/pkg/blockchain/msg_sender.go +++ b/pkg/blockchain/msg_sender.go @@ -19,10 +19,18 @@ import ( const ttl = 5 * 60 // in seconds +// MessageSender is one blockchain connection an external message can be +// broadcast through. MsgSender holds several - one per liteserver - because a +// message wants to reach as many validators as it can, which is the opposite of +// what a pool that picks a single best connection does for you. +type MessageSender interface { + SendMessage(ctx context.Context, payload []byte) (uint32, error) +} + // MsgSender provides a method to send a message to the blockchain. type MsgSender struct { logger *zap.Logger - sendingClients []*liteapi.Client + sendingClients []MessageSender // receivers get a copy of a message before sending it to the blockchain. // receivers is a read-only map/field. receivers map[string]chan<- ExtInMsgCopy @@ -95,6 +103,20 @@ func NewMsgSender(logger *zap.Logger, servers []config.LiteServer, receivers map if len(clients) == 0 { return nil, fmt.Errorf("no lite clients available") } + senders := make([]MessageSender, 0, len(clients)) + for _, cli := range clients { + senders = append(senders, cli) + } + return NewMsgSenderWithClients(logger, senders, receivers) +} + +// NewMsgSenderWithClients builds a sender over connections the caller supplies, +// one per liteserver it wants messages broadcast to, for a deployment whose +// connections are not ones this package can open from a list of liteservers. +func NewMsgSenderWithClients(logger *zap.Logger, clients []MessageSender, receivers map[string]chan<- ExtInMsgCopy) (*MsgSender, error) { + if len(clients) == 0 { + return nil, fmt.Errorf("no lite clients available") + } msgSender := &MsgSender{ sendingClients: clients, diff --git a/pkg/blockchain/msg_sender_test.go b/pkg/blockchain/msg_sender_test.go index 03b94d85..549a2b4f 100644 --- a/pkg/blockchain/msg_sender_test.go +++ b/pkg/blockchain/msg_sender_test.go @@ -76,7 +76,7 @@ func TestMsgSender_send_AllAttemptsFail(t *testing.T) { ) require.NoError(t, err) - ms := &MsgSender{sendingClients: []*liteapi.Client{client}} + ms := &MsgSender{sendingClients: []MessageSender{client}} sendCtx, sendCancel := context.WithTimeout(context.Background(), 5*time.Second) defer sendCancel() diff --git a/pkg/core/elector.go b/pkg/core/elector.go index 31085f18..168be5d4 100644 --- a/pkg/core/elector.go +++ b/pkg/core/elector.go @@ -10,7 +10,6 @@ import ( "slices" "time" - "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" ) @@ -22,7 +21,7 @@ type ElectorRoundsIterator struct { } type ValidatorSetIterator struct { - *liteapi.Client + LiteClient ctx context.Context err error } @@ -72,14 +71,14 @@ type ElectorData struct { ActiveHash tlb.Bits256 } -func NewElectorRoundsIterator(ctx context.Context, client *liteapi.Client) ElectorRoundsIterator { +func NewElectorRoundsIterator(ctx context.Context, client LiteClient) ElectorRoundsIterator { return ElectorRoundsIterator{ NewValidatorSetIterator(ctx, client), } } -func NewValidatorSetIterator(ctx context.Context, client *liteapi.Client) *ValidatorSetIterator { - return &ValidatorSetIterator{ctx: ctx, Client: client} +func NewValidatorSetIterator(ctx context.Context, client LiteClient) *ValidatorSetIterator { + return &ValidatorSetIterator{ctx: ctx, LiteClient: client} } func (cli *ElectorRoundsIterator) Run(yield func(ElectorRound) bool) { diff --git a/pkg/core/liteclient.go b/pkg/core/liteclient.go new file mode 100644 index 00000000..3d6cf12b --- /dev/null +++ b/pkg/core/liteclient.go @@ -0,0 +1,84 @@ +package core + +import ( + "context" + + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/liteclient" + "github.com/tonkeeper/tongo/tep64" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +// LiteClient is the blockchain connection everything here reads through: +// litestorage, the elector iterators, the rewards service. It is deliberately +// an interface rather than tongo's *liteapi.Client, so that a deployment can +// supply a different implementation - one talking to its own pool of +// lightservers, say - without that implementation having to live in this +// repository. +// +// The method set is tongo's, so *liteapi.Client is one line away from +// satisfying it; see LiteAPIClient for the line in question. +type LiteClient interface { + // Masterchain and blocks. + GetMasterchainInfo(ctx context.Context) (liteclient.LiteServerMasterchainInfoC, error) + GetMasterchainInfoExt(ctx context.Context, mode uint32) (liteclient.LiteServerMasterchainInfoExtC, error) + GetTime(ctx context.Context) (uint32, error) + LookupBlock(ctx context.Context, blockID ton.BlockID, mode uint32, lt *uint64, utime *uint32) (ton.BlockIDExt, tlb.BlockInfo, error) + GetBlock(ctx context.Context, blockID ton.BlockIDExt) (tlb.Block, error) + GetBlockRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerBlockDataC, error) + GetBlockHeaderRaw(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (liteclient.LiteServerBlockHeaderC, error) + GetStateRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerBlockStateC, error) + GetBlockProofRaw(ctx context.Context, knownBlock ton.BlockIDExt, targetBlock *ton.BlockIDExt) (liteclient.LiteServerPartialBlockProofC, error) + GetShardBlockProofRaw(ctx context.Context) (liteclient.LiteServerShardBlockProofC, error) + GetAllShardsInfo(ctx context.Context, blockID ton.BlockIDExt) ([]ton.BlockIDExt, error) + GetAllShardsInfoRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerAllShardsInfoC, error) + GetShardInfoRaw(ctx context.Context, blockID ton.BlockIDExt, workchain uint32, shard uint64, exact bool) (liteclient.LiteServerShardInfoC, error) + GetOutMsgQueueSizes(ctx context.Context) (liteclient.LiteServerOutMsgQueueSizesC, error) + + // Accounts and transactions. + GetAccountState(ctx context.Context, accountID ton.AccountID) (tlb.ShardAccount, error) + GetAccountStateRaw(ctx context.Context, accountID ton.AccountID) (liteclient.LiteServerAccountStateC, error) + GetSeqno(ctx context.Context, account ton.AccountID) (uint32, error) + GetLastTransactions(ctx context.Context, a ton.AccountID, limit int) ([]ton.Transaction, error) + GetTransactionsRaw(ctx context.Context, count uint32, accountID ton.AccountID, lt uint64, hash ton.Bits256) (liteclient.LiteServerTransactionListC, error) + ListBlockTransactionsRaw(ctx context.Context, blockID ton.BlockIDExt, mode, count uint32, after *liteclient.LiteServerTransactionId3C) (liteclient.LiteServerBlockTransactionsC, error) + + // Config, libraries and the VM. + GetConfigAll(ctx context.Context, mode liteapi.ConfigMode) (tlb.ConfigParams, error) + GetConfigAllRaw(ctx context.Context, mode liteapi.ConfigMode) (liteclient.LiteServerConfigInfoC, error) + GetConfigParams(ctx context.Context, mode liteapi.ConfigMode, paramList []uint32) (tlb.ConfigParams, error) + GetLibraries(ctx context.Context, libraryList []ton.Bits256) (map[ton.Bits256]*boc.Cell, error) + GetJettonData(ctx context.Context, master ton.AccountID) (tep64.Metadata, error) + RunSmcMethod(ctx context.Context, accountID ton.AccountID, method string, params tlb.VmStack) (uint32, tlb.VmStack, error) + RunSmcMethodByID(ctx context.Context, accountID ton.AccountID, methodID int, params tlb.VmStack) (uint32, tlb.VmStack, error) + + // SendMessage broadcasts an external message. + SendMessage(ctx context.Context, payload []byte) (uint32, error) + + // WithBlock pins the queries made through the returned client to a block, + // for the reads that must be answered as of one block rather than as of + // whenever each of them happened to be served. + WithBlock(blockID ton.BlockIDExt) LiteClient +} + +// liteAPIClient adapts tongo's *liteapi.Client to LiteClient. +// +// Exactly one method needs writing. Go has no covariant returns, so the +// concrete client's WithBlock, which returns *liteapi.Client, cannot satisfy an +// interface method returning LiteClient. Every other method is promoted from +// the embedded client unchanged - the interface was written to tongo's +// signatures so that this stays true. +type liteAPIClient struct { + *liteapi.Client +} + +// LiteAPIClient wraps a tongo lite client so it can be used as a LiteClient. +func LiteAPIClient(cli *liteapi.Client) LiteClient { + return liteAPIClient{Client: cli} +} + +func (c liteAPIClient) WithBlock(blockID ton.BlockIDExt) LiteClient { + return liteAPIClient{Client: c.Client.WithBlock(blockID)} +} diff --git a/pkg/litestorage/jetton_test.go b/pkg/litestorage/jetton_test.go index 7bd21342..c4b1e640 100644 --- a/pkg/litestorage/jetton_test.go +++ b/pkg/litestorage/jetton_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/liteapi" "go.uber.org/zap" @@ -22,7 +23,7 @@ func TestGetJettonMasterData_PopulatesHashes(t *testing.T) { cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.NoError(t, err) - storage, err := NewLiteStorage(logger, cli) + storage, err := NewLiteStorage(logger, core.LiteAPIClient(cli)) require.NoError(t, err) // Use a well-known jetton master (KINGYTON from existing tests). diff --git a/pkg/litestorage/libraries_test.go b/pkg/litestorage/libraries_test.go index fb86c619..0f290570 100644 --- a/pkg/litestorage/libraries_test.go +++ b/pkg/litestorage/libraries_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/liteapi" "go.uber.org/zap" @@ -19,7 +20,7 @@ func TestLiteStorage_GetLibraries(t *testing.T) { cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) - storage, err := NewLiteStorage(zap.L(), cli) + storage, err := NewLiteStorage(zap.L(), core.LiteAPIClient(cli)) require.Nil(t, err) libs := []tongo.Bits256{ diff --git a/pkg/litestorage/litestorage.go b/pkg/litestorage/litestorage.go index a986bb3f..f64dd7c3 100644 --- a/pkg/litestorage/litestorage.go +++ b/pkg/litestorage/litestorage.go @@ -22,7 +22,6 @@ import ( "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/abi" "github.com/tonkeeper/tongo/boc" - "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tep64" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" @@ -63,7 +62,7 @@ type PriceFeeds interface { type LiteStorage struct { logger *zap.Logger - client *liteapi.Client + client core.LiteClient executor abi.Executor jettonMetaCache *xsync.MapOf[string, tep64.Metadata] transactionsIndexByHash *xsync.MapOf[tongo.Bits256, *core.Transaction] @@ -149,7 +148,7 @@ func WithBlockChannel(ch <-chan indexer.IDandBlock) Option { type Option func(o *Options) -func NewLiteStorage(log *zap.Logger, cli *liteapi.Client, opts ...Option) (*LiteStorage, error) { +func NewLiteStorage(log *zap.Logger, cli core.LiteClient, opts ...Option) (*LiteStorage, error) { o := &Options{} for i := range opts { opts[i](o) diff --git a/pkg/litestorage/litestorage_test.go b/pkg/litestorage/litestorage_test.go index b175bb1e..1f8466f1 100644 --- a/pkg/litestorage/litestorage_test.go +++ b/pkg/litestorage/litestorage_test.go @@ -90,7 +90,7 @@ func TestLiteStorage_runBlockchainConfigUpdate(t *testing.T) { require.Nil(t, err) s := &LiteStorage{ logger: zap.L(), - client: cli, + client: core.LiteAPIClient(cli), } s.runBlockchainConfigUpdate(100 * time.Millisecond) @@ -105,7 +105,7 @@ func TestLiteStorage_TrimmedConfigBase64(t *testing.T) { require.Nil(t, err) s := &LiteStorage{ logger: zap.L(), - client: cli, + client: core.LiteAPIClient(cli), } conf := s.blockchainConfig() require.Empty(t, conf) diff --git a/pkg/litestorage/trace_test.go b/pkg/litestorage/trace_test.go index b2f66ece..d7252530 100644 --- a/pkg/litestorage/trace_test.go +++ b/pkg/litestorage/trace_test.go @@ -7,6 +7,7 @@ import ( "github.com/puzpuzpuz/xsync/v2" "github.com/stretchr/testify/require" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo" "github.com/tonkeeper/tongo/abi" "github.com/tonkeeper/tongo/liteapi" @@ -20,7 +21,7 @@ func TestLiteStorage_getAccountInterfaces(t *testing.T) { cli, err := liteapi.NewClient(liteapi.FromEnvsOrMainnet()) require.Nil(t, err) storage := LiteStorage{ - client: cli, + client: core.LiteAPIClient(cli), executor: cli, accountInterfacesCache: xsync.NewTypedMapOf[tongo.AccountID, []abi.ContractInterface](hashAccountID), } diff --git a/pkg/rewards/service/roundrobin.go b/pkg/rewards/service/roundrobin.go index 4e9cc21a..bc6caab2 100644 --- a/pkg/rewards/service/roundrobin.go +++ b/pkg/rewards/service/roundrobin.go @@ -6,6 +6,7 @@ import ( "log" "sync/atomic" + "github.com/tonkeeper/opentonapi/pkg/core" "github.com/tonkeeper/tongo/config" "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/liteclient" @@ -21,16 +22,13 @@ const tracerName = "github.com/tonkeeper/opentonapi/pkg/validatorsrewards/servic // LiteClient is the interface for blockchain operations. Implemented by // RoundRobinClient and used by all service methods. -type LiteClient interface { - GetMasterchainInfo(context.Context) (liteclient.LiteServerMasterchainInfoC, error) - LookupBlock(context.Context, ton.BlockID, uint32, *uint64, *uint32) (ton.BlockIDExt, tlb.BlockInfo, error) - GetBlock(context.Context, ton.BlockIDExt) (tlb.Block, error) - GetAccountState(context.Context, ton.AccountID) (tlb.ShardAccount, error) - GetConfigParams(context.Context, liteapi.ConfigMode, []uint32) (tlb.ConfigParams, error) - RunSmcMethodByID(context.Context, ton.AccountID, int, tlb.VmStack) (uint32, tlb.VmStack, error) - RunSmcMethod(context.Context, ton.AccountID, string, tlb.VmStack) (uint32, tlb.VmStack, error) - WithBlock(ton.BlockIDExt) LiteClient -} +// +// It is core.LiteClient rather than a narrower interface of its own: two +// self-returning interfaces over the same client cannot be substituted for each +// other in Go, which has no covariant returns, so a second one here would mean +// the rewards service was the one place a deployment could not supply its own +// blockchain connection. +type LiteClient = core.LiteClient // clientEntry holds a liteapi client and its metadata for debug logging. type clientEntry struct { diff --git a/pkg/rewards/service/roundrobin_delegates.go b/pkg/rewards/service/roundrobin_delegates.go new file mode 100644 index 00000000..d13d52e2 --- /dev/null +++ b/pkg/rewards/service/roundrobin_delegates.go @@ -0,0 +1,168 @@ +package service + +import ( + "context" + + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/liteclient" + "github.com/tonkeeper/tongo/tep64" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// The methods in this file complete RoundRobinClient's LiteClient contract. +// The rewards service does not call any of them - the ones it does call live in +// roundrobin.go, instrumented with the attributes that are worth having for +// them. These exist so that a RoundRobinClient is a whole blockchain client and +// not a special case, which is what lets the service take the same LiteClient +// as everything else. +// +// They are deliberately uniform: pick a connection, delegate, record the error. +// Anything that needs more than that belongs in roundrobin.go with the rest. + +// delegate runs one pass-through call inside a span named for the method, +// noting the pinned block when the client is pinned to one. +func delegate[T any](ctx context.Context, r *RoundRobinClient, method string, fn func(context.Context, *liteapi.Client) (T, error)) (T, error) { + ctx, span := otel.Tracer(tracerName).Start(ctx, "liteclient."+method, trace.WithSpanKind(trace.SpanKindClient)) + if r.targetBlock != nil { + span.SetAttributes(attribute.Int64("ton.block.seqno", int64(r.targetBlock.BlockID.Seqno))) + } + defer span.End() + res, err := fn(ctx, r.clientForRequest(ctx)) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + return res, err +} + +func (r *RoundRobinClient) GetMasterchainInfoExt(ctx context.Context, mode uint32) (liteclient.LiteServerMasterchainInfoExtC, error) { + return delegate(ctx, r, "GetMasterchainInfoExt", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerMasterchainInfoExtC, error) { + return c.GetMasterchainInfoExt(ctx, mode) + }) +} + +func (r *RoundRobinClient) GetTime(ctx context.Context) (uint32, error) { + return delegate(ctx, r, "GetTime", func(ctx context.Context, c *liteapi.Client) (uint32, error) { + return c.GetTime(ctx) + }) +} + +func (r *RoundRobinClient) GetBlockRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerBlockDataC, error) { + return delegate(ctx, r, "GetBlockRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerBlockDataC, error) { + return c.GetBlockRaw(ctx, blockID) + }) +} + +func (r *RoundRobinClient) GetBlockHeaderRaw(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (liteclient.LiteServerBlockHeaderC, error) { + return delegate(ctx, r, "GetBlockHeaderRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerBlockHeaderC, error) { + return c.GetBlockHeaderRaw(ctx, blockID, mode) + }) +} + +func (r *RoundRobinClient) GetStateRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerBlockStateC, error) { + return delegate(ctx, r, "GetStateRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerBlockStateC, error) { + return c.GetStateRaw(ctx, blockID) + }) +} + +func (r *RoundRobinClient) GetBlockProofRaw(ctx context.Context, knownBlock ton.BlockIDExt, targetBlock *ton.BlockIDExt) (liteclient.LiteServerPartialBlockProofC, error) { + return delegate(ctx, r, "GetBlockProofRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerPartialBlockProofC, error) { + return c.GetBlockProofRaw(ctx, knownBlock, targetBlock) + }) +} + +func (r *RoundRobinClient) GetShardBlockProofRaw(ctx context.Context) (liteclient.LiteServerShardBlockProofC, error) { + return delegate(ctx, r, "GetShardBlockProofRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerShardBlockProofC, error) { + return c.GetShardBlockProofRaw(ctx) + }) +} + +func (r *RoundRobinClient) GetAllShardsInfo(ctx context.Context, blockID ton.BlockIDExt) ([]ton.BlockIDExt, error) { + return delegate(ctx, r, "GetAllShardsInfo", func(ctx context.Context, c *liteapi.Client) ([]ton.BlockIDExt, error) { + return c.GetAllShardsInfo(ctx, blockID) + }) +} + +func (r *RoundRobinClient) GetAllShardsInfoRaw(ctx context.Context, blockID ton.BlockIDExt) (liteclient.LiteServerAllShardsInfoC, error) { + return delegate(ctx, r, "GetAllShardsInfoRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerAllShardsInfoC, error) { + return c.GetAllShardsInfoRaw(ctx, blockID) + }) +} + +func (r *RoundRobinClient) GetShardInfoRaw(ctx context.Context, blockID ton.BlockIDExt, workchain uint32, shard uint64, exact bool) (liteclient.LiteServerShardInfoC, error) { + return delegate(ctx, r, "GetShardInfoRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerShardInfoC, error) { + return c.GetShardInfoRaw(ctx, blockID, workchain, shard, exact) + }) +} + +func (r *RoundRobinClient) GetOutMsgQueueSizes(ctx context.Context) (liteclient.LiteServerOutMsgQueueSizesC, error) { + return delegate(ctx, r, "GetOutMsgQueueSizes", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerOutMsgQueueSizesC, error) { + return c.GetOutMsgQueueSizes(ctx) + }) +} + +func (r *RoundRobinClient) GetAccountStateRaw(ctx context.Context, accountID ton.AccountID) (liteclient.LiteServerAccountStateC, error) { + return delegate(ctx, r, "GetAccountStateRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerAccountStateC, error) { + return c.GetAccountStateRaw(ctx, accountID) + }) +} + +func (r *RoundRobinClient) GetSeqno(ctx context.Context, account ton.AccountID) (uint32, error) { + return delegate(ctx, r, "GetSeqno", func(ctx context.Context, c *liteapi.Client) (uint32, error) { + return c.GetSeqno(ctx, account) + }) +} + +func (r *RoundRobinClient) GetLastTransactions(ctx context.Context, a ton.AccountID, limit int) ([]ton.Transaction, error) { + return delegate(ctx, r, "GetLastTransactions", func(ctx context.Context, c *liteapi.Client) ([]ton.Transaction, error) { + return c.GetLastTransactions(ctx, a, limit) + }) +} + +func (r *RoundRobinClient) GetTransactionsRaw(ctx context.Context, count uint32, accountID ton.AccountID, lt uint64, hash ton.Bits256) (liteclient.LiteServerTransactionListC, error) { + return delegate(ctx, r, "GetTransactionsRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerTransactionListC, error) { + return c.GetTransactionsRaw(ctx, count, accountID, lt, hash) + }) +} + +func (r *RoundRobinClient) ListBlockTransactionsRaw(ctx context.Context, blockID ton.BlockIDExt, mode, count uint32, after *liteclient.LiteServerTransactionId3C) (liteclient.LiteServerBlockTransactionsC, error) { + return delegate(ctx, r, "ListBlockTransactionsRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerBlockTransactionsC, error) { + return c.ListBlockTransactionsRaw(ctx, blockID, mode, count, after) + }) +} + +func (r *RoundRobinClient) GetConfigAll(ctx context.Context, mode liteapi.ConfigMode) (tlb.ConfigParams, error) { + return delegate(ctx, r, "GetConfigAll", func(ctx context.Context, c *liteapi.Client) (tlb.ConfigParams, error) { + return c.GetConfigAll(ctx, mode) + }) +} + +func (r *RoundRobinClient) GetConfigAllRaw(ctx context.Context, mode liteapi.ConfigMode) (liteclient.LiteServerConfigInfoC, error) { + return delegate(ctx, r, "GetConfigAllRaw", func(ctx context.Context, c *liteapi.Client) (liteclient.LiteServerConfigInfoC, error) { + return c.GetConfigAllRaw(ctx, mode) + }) +} + +func (r *RoundRobinClient) GetLibraries(ctx context.Context, libraryList []ton.Bits256) (map[ton.Bits256]*boc.Cell, error) { + return delegate(ctx, r, "GetLibraries", func(ctx context.Context, c *liteapi.Client) (map[ton.Bits256]*boc.Cell, error) { + return c.GetLibraries(ctx, libraryList) + }) +} + +func (r *RoundRobinClient) GetJettonData(ctx context.Context, master ton.AccountID) (tep64.Metadata, error) { + return delegate(ctx, r, "GetJettonData", func(ctx context.Context, c *liteapi.Client) (tep64.Metadata, error) { + return c.GetJettonData(ctx, master) + }) +} + +func (r *RoundRobinClient) SendMessage(ctx context.Context, payload []byte) (uint32, error) { + return delegate(ctx, r, "SendMessage", func(ctx context.Context, c *liteapi.Client) (uint32, error) { + return c.SendMessage(ctx, payload) + }) +} diff --git a/pkg/rewards/service/service.go b/pkg/rewards/service/service.go index 0ac5dbcb..e65fe821 100644 --- a/pkg/rewards/service/service.go +++ b/pkg/rewards/service/service.go @@ -33,6 +33,12 @@ func (s *Service) currentClient() LiteClient { if !needsRefresh { return client } + // A client supplied by the caller comes with no server list to rebuild one + // from. It keeps its own connections healthy, so there is nothing here to + // refresh it with, and trying would log a failure on every call. + if len(s.liteServers) == 0 { + return client + } s.mu.Lock() defer s.mu.Unlock() diff --git a/pkg/rewards/service/stats.go b/pkg/rewards/service/stats.go index 90012187..b3722a74 100644 --- a/pkg/rewards/service/stats.go +++ b/pkg/rewards/service/stats.go @@ -21,7 +21,7 @@ var errServiceUnavailable = errors.New("Service Unavailable") type Stats struct { mu sync.RWMutex options []liteapi.Option - client *liteapi.Client + client LiteClient rounds []core.ElectorRound } @@ -37,6 +37,17 @@ func NewStats(options ...liteapi.Option) *Stats { return res } +// NewStatsWithClient builds the statistics service on a blockchain connection +// the caller supplies, for a deployment whose connection is not one this +// package can construct from a list of liteservers. +func NewStatsWithClient(client LiteClient) *Stats { + res := &Stats{client: client} + if client != nil { + go res.updateProc() + } + return res +} + func (s *Stats) GetAPY(poolAPYMul float64) float64 { s.mu.RLock() defer s.mu.RUnlock() @@ -116,12 +127,12 @@ func (s *Stats) updateProc() { func (s *Stats) update() { if s.client == nil { - var err error - s.client, err = liteapi.NewClient(s.options...) + cli, err := liteapi.NewClient(s.options...) if err != nil { log.Println("rewards stats service:", err) return } + s.client = core.LiteAPIClient(cli) } iter := core.NewElectorRoundsIterator(context.Background(), s.client) for v := range iter.Run {