diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 2a5a7edff1..0272f87593 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -66,7 +66,7 @@ type Opt func(*Ticker) // WithLogger sets the logger for the Ticker. func WithLogger(log zerolog.Logger, name string) Opt { return func(t *Ticker) { - t.logger = log.With().Str("ticker_name", name).Logger() + t.logger = log.With().Str("ticker.name", name).Logger() } } @@ -82,6 +82,7 @@ func New(interval time.Duration, task Task, opts ...Opt) *Ticker { t := &Ticker{ interval: interval, task: task, + logger: zerolog.Nop(), } for _, opt := range opts { @@ -162,6 +163,11 @@ func (t *Ticker) SetInterval(interval time.Duration) { return } + t.logger.Info(). + Dur("ticker.old_interval", t.interval). + Dur("ticker.new_interval", interval). + Msg("Changing interval") + t.interval = interval t.ticker.Reset(interval) } diff --git a/zetaclient/chains/ton/config.go b/zetaclient/chains/ton/config.go index 731287756e..b0fc9906a2 100644 --- a/zetaclient/chains/ton/config.go +++ b/zetaclient/chains/ton/config.go @@ -7,7 +7,10 @@ import ( "net/url" "time" + "github.com/pkg/errors" "github.com/tonkeeper/tongo/config" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/tlb" ) type GlobalConfigurationFile = config.GlobalConfigurationFile @@ -52,3 +55,51 @@ func ConfigFromSource(ctx context.Context, urlOrPath string) (*GlobalConfigurati return ConfigFromPath(urlOrPath) } + +// ConfigGetter represents LiteAPI config getter. +type ConfigGetter interface { + GetConfigParams(ctx context.Context, mode liteapi.ConfigMode, params []uint32) (tlb.ConfigParams, error) +} + +// FetchGasConfig fetches gas price from the config. +func FetchGasConfig(ctx context.Context, getter ConfigGetter) (tlb.GasLimitsPrices, error) { + // https://docs.ton.org/develop/howto/blockchain-configs + // https://tonviewer.com/config#21 + const configKeyGas = 21 + + response, err := getter.GetConfigParams(ctx, 0, []uint32{configKeyGas}) + if err != nil { + return tlb.GasLimitsPrices{}, errors.Wrap(err, "failed to get config params") + } + + ref, ok := response.Config.Get(configKeyGas) + if !ok { + return tlb.GasLimitsPrices{}, errors.Errorf("config key %d not found", configKeyGas) + } + + var cfg tlb.ConfigParam21 + if err = tlb.Unmarshal(&ref.Value, &cfg); err != nil { + return tlb.GasLimitsPrices{}, errors.Wrap(err, "failed to unmarshal config param") + } + + return cfg.GasLimitsPrices, nil +} + +// ParseGasPrice parses gas price from the config and returns price in tons per 1 gas unit. +func ParseGasPrice(cfg tlb.GasLimitsPrices) (uint64, error) { + // from TON docs: gas_price: This parameter reflects + // the price of gas in the network, in nano tons per 65536 gas units (2^16). + switch cfg.SumType { + case "GasPrices": + return cfg.GasPrices.GasPrice >> 16, nil + case "GasPricesExt": + return cfg.GasPricesExt.GasPrice >> 16, nil + case "GasFlatPfx": + if cfg.GasFlatPfx.Other == nil { + return 0, errors.New("GasFlatPfx.Other is nil") + } + return ParseGasPrice(*cfg.GasFlatPfx.Other) + default: + return 0, errors.Errorf("unknown SumType: %q", cfg.SumType) + } +} diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index 5ee22da113..f77f08420b 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -8,12 +8,15 @@ import ( "testing" "time" + "cosmossdk.io/math" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/config" "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + zetaton "github.com/zeta-chain/node/zetaclient/chains/ton" "github.com/zeta-chain/node/zetaclient/common" ) @@ -164,6 +167,27 @@ func TestClient(t *testing.T) { t.Logf("Masterchain block #%d is generated at %q (%s ago)", block.SeqNo, blockTime, since.String()) }) + + t.Run("GetGasConfig", func(t *testing.T) { + // ACT #1 + gas, err := zetaton.FetchGasConfig(ctx, client) + + // ASSERT #1 + require.NoError(t, err) + require.NotEmpty(t, gas) + + // ACT #2 + gasPrice, err := zetaton.ParseGasPrice(gas) + + // ASSERT #2 + require.NoError(t, err) + require.NotEmpty(t, gasPrice) + + gasPricePer1000 := math.NewUint(1000 * gasPrice) + + t.Logf("Gas cost: %s per 1000 gas", toncontracts.FormatCoins(gasPricePer1000)) + t.Logf("Compare with https://tonwhales.com/explorer/network") + }) } func mustCreateClient(t *testing.T) *liteapi.Client { diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index b1ade8ac6c..99d9ff299c 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -16,6 +16,7 @@ import ( "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" + zetaton "github.com/zeta-chain/node/zetaclient/chains/ton" "github.com/zeta-chain/node/zetaclient/common" ) @@ -28,9 +29,11 @@ type Observer struct { } // LiteClient represents a TON client +// see https://github.com/ton-blockchain/ton/blob/master/tl/generate/scheme/tonlib_api.tl // //go:generate mockery --name LiteClient --filename ton_liteclient.go --case underscore --output ../../../testutils/mocks type LiteClient interface { + zetaton.ConfigGetter GetMasterchainInfo(ctx context.Context) (liteclient.LiteServerMasterchainInfoC, error) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) @@ -88,9 +91,54 @@ func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossCha } // watchGasPrice observes TON gas price and votes it to Zetacore. -func (ob *Observer) watchGasPrice(_ context.Context) error { - // todo implement me - return nil +func (ob *Observer) watchGasPrice(ctx context.Context) error { + task := func(ctx context.Context, t *ticker.Ticker) error { + if err := ob.postGasPrice(ctx); err != nil { + ob.Logger().GasPrice.Err(err).Msg("reportGasPrice error") + } + + newInternal := ticker.SecondsFromUint64(ob.ChainParams().GasPriceTicker) + t.SetInterval(newInternal) + + return nil + } + + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started") + + return ticker.Run( + ctx, + ticker.SecondsFromUint64(ob.ChainParams().GasPriceTicker), + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().GasPrice, "WatchGasPrice"), + ) +} + +// postGasPrice fetches on-chain gas config and reports it to Zetacore. +func (ob *Observer) postGasPrice(ctx context.Context) error { + cfg, err := zetaton.FetchGasConfig(ctx, ob.client) + if err != nil { + return errors.Wrap(err, "failed to fetch gas config") + } + + gasPrice, err := zetaton.ParseGasPrice(cfg) + if err != nil { + return errors.Wrap(err, "failed to parse gas price") + } + + blockID, err := ob.getLatestMasterchainBlock(ctx) + if err != nil { + return errors.Wrap(err, "failed to get latest masterchain block") + } + + // There's no concept of priority fee in TON + const priorityFee = 0 + + _, errVote := ob. + ZetacoreClient(). + PostVoteGasPrice(ctx, ob.Chain(), gasPrice, priorityFee, uint64(blockID.Seqno)) + + return errVote } // watchRPCStatus observes TON RPC status. @@ -114,20 +162,18 @@ func (ob *Observer) watchRPCStatus(ctx context.Context) error { // checkRPCStatus checks TON RPC status and alerts if necessary. func (ob *Observer) checkRPCStatus(ctx context.Context) error { - mc, err := ob.client.GetMasterchainInfo(ctx) + blockID, err := ob.getLatestMasterchainBlock(ctx) if err != nil { - return errors.Wrap(err, "failed to get masterchain info") + return errors.Wrap(err, "failed to get latest masterchain block") } - blockID := mc.Last - - block, err := ob.client.GetBlockHeader(ctx, blockID.ToBlockIdExt(), 0) + block, err := ob.client.GetBlockHeader(ctx, blockID, 0) if err != nil { return errors.Wrap(err, "failed to get masterchain block header") } if block.NotMaster { - return errors.Errorf("block %q is not a master block", blockID.ToBlockIdExt().BlockID.String()) + return errors.Errorf("block %q is not a master block", blockID.BlockID.String()) } blockTime := time.Unix(int64(block.GenUtime), 0).UTC() @@ -139,3 +185,12 @@ func (ob *Observer) checkRPCStatus(ctx context.Context) error { return nil } + +func (ob *Observer) getLatestMasterchainBlock(ctx context.Context) (ton.BlockIDExt, error) { + mc, err := ob.client.GetMasterchainInfo(ctx) + if err != nil { + return ton.BlockIDExt{}, errors.Wrap(err, "failed to get masterchain info") + } + + return mc.Last.ToBlockIdExt(), nil +} diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go index b5f028d984..1f73e4a54a 100644 --- a/zetaclient/testutils/mocks/ton_liteclient.go +++ b/zetaclient/testutils/mocks/ton_liteclient.go @@ -5,9 +5,11 @@ package mocks import ( context "context" - mock "github.com/stretchr/testify/mock" + liteapi "github.com/tonkeeper/tongo/liteapi" liteclient "github.com/tonkeeper/tongo/liteclient" + mock "github.com/stretchr/testify/mock" + tlb "github.com/tonkeeper/tongo/tlb" ton "github.com/tonkeeper/tongo/ton" @@ -46,6 +48,34 @@ func (_m *LiteClient) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt return r0, r1 } +// GetConfigParams provides a mock function with given fields: ctx, mode, params +func (_m *LiteClient) GetConfigParams(ctx context.Context, mode liteapi.ConfigMode, params []uint32) (tlb.ConfigParams, error) { + ret := _m.Called(ctx, mode, params) + + if len(ret) == 0 { + panic("no return value specified for GetConfigParams") + } + + var r0 tlb.ConfigParams + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, liteapi.ConfigMode, []uint32) (tlb.ConfigParams, error)); ok { + return rf(ctx, mode, params) + } + if rf, ok := ret.Get(0).(func(context.Context, liteapi.ConfigMode, []uint32) tlb.ConfigParams); ok { + r0 = rf(ctx, mode, params) + } else { + r0 = ret.Get(0).(tlb.ConfigParams) + } + + if rf, ok := ret.Get(1).(func(context.Context, liteapi.ConfigMode, []uint32) error); ok { + r1 = rf(ctx, mode, params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetFirstTransaction provides a mock function with given fields: ctx, id func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) { ret := _m.Called(ctx, id)